feat: ビジュアライズと設定
This commit is contained in:
parent
af4008ee45
commit
8f6f505cb4
2
Lands
2
Lands
|
|
@ -1 +1 @@
|
||||||
Subproject commit e24484bdea95f6dd1d9aeef538c01c0c7b9db1f1
|
Subproject commit d27057dc89e228ce120c5b5b4534b3c130e8480a
|
||||||
|
|
@ -4,6 +4,7 @@ import net.hareworks.hcu.core.Main
|
||||||
import net.hareworks.hcu.core.player.PlayerIdService
|
import net.hareworks.hcu.core.player.PlayerIdService
|
||||||
import net.hareworks.hcu.landsector.command.LandSectorCommand
|
import net.hareworks.hcu.landsector.command.LandSectorCommand
|
||||||
import net.hareworks.hcu.landsector.listener.SectorListener
|
import net.hareworks.hcu.landsector.listener.SectorListener
|
||||||
|
import net.hareworks.hcu.landsector.service.SelectionService
|
||||||
import net.hareworks.hcu.landsector.service.SectorService
|
import net.hareworks.hcu.landsector.service.SectorService
|
||||||
import org.bukkit.plugin.java.JavaPlugin
|
import org.bukkit.plugin.java.JavaPlugin
|
||||||
|
|
||||||
|
|
@ -15,10 +16,13 @@ class LandSectorPlugin : JavaPlugin() {
|
||||||
}
|
}
|
||||||
|
|
||||||
var sectorService: SectorService? = null
|
var sectorService: SectorService? = null
|
||||||
|
var selectionService: SelectionService? = null
|
||||||
|
|
||||||
override fun onEnable() {
|
override fun onEnable() {
|
||||||
instance = this
|
instance = this
|
||||||
|
|
||||||
|
selectionService = SelectionService()
|
||||||
|
|
||||||
// Register commands
|
// Register commands
|
||||||
LandSectorCommand(this).register()
|
LandSectorCommand(this).register()
|
||||||
|
|
||||||
|
|
@ -57,12 +61,30 @@ class LandSectorPlugin : JavaPlugin() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
server.pluginManager.registerEvents(SectorListener(this, service, pIdService), this)
|
val selService = selectionService
|
||||||
|
if (selService != null) {
|
||||||
|
server.pluginManager.registerEvents(SectorListener(this, service, pIdService, selService), this)
|
||||||
|
server.pluginManager.registerEvents(net.hareworks.hcu.landsector.listener.SelectionListener(this, selService, service), this)
|
||||||
|
|
||||||
|
// Schedule visualization task
|
||||||
|
net.hareworks.hcu.landsector.task.SelectionVisualizerTask(this, selService).runTaskTimer(this, 0L, 4L)
|
||||||
|
} else {
|
||||||
|
logger.severe("SelectionService not initialized!")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schedule auto-save every 5 minutes
|
||||||
|
server.scheduler.runTaskTimerAsynchronously(this, Runnable {
|
||||||
|
service.flushChanges()
|
||||||
|
}, 6000L, 6000L)
|
||||||
|
|
||||||
|
// Schedule rotation task
|
||||||
|
net.hareworks.hcu.landsector.task.SectorRotationTask(this).runTaskTimer(this, 1L, 1L)
|
||||||
|
|
||||||
logger.info("LandSector initialized with services.")
|
logger.info("LandSector initialized with services.")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDisable() {
|
override fun onDisable() {
|
||||||
|
sectorService?.flushChanges()
|
||||||
logger.info("LandSector plugin has been disabled!")
|
logger.info("LandSector plugin has been disabled!")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,62 @@
|
||||||
package net.hareworks.hcu.landsector.command
|
package net.hareworks.hcu.landsector.command
|
||||||
|
|
||||||
import net.hareworks.hcu.landsector.LandSectorPlugin
|
import net.hareworks.hcu.landsector.LandSectorPlugin
|
||||||
|
import net.hareworks.hcu.landsector.model.Sector
|
||||||
import net.hareworks.kommand_lib.kommand
|
import net.hareworks.kommand_lib.kommand
|
||||||
import net.kyori.adventure.text.Component
|
import net.kyori.adventure.text.Component
|
||||||
import net.kyori.adventure.text.format.NamedTextColor
|
import net.kyori.adventure.text.format.NamedTextColor
|
||||||
|
import org.bukkit.Bukkit
|
||||||
|
import org.bukkit.Location
|
||||||
import org.bukkit.Material
|
import org.bukkit.Material
|
||||||
import org.bukkit.NamespacedKey
|
import org.bukkit.NamespacedKey
|
||||||
import org.bukkit.entity.Player
|
import org.bukkit.entity.Player
|
||||||
|
import org.bukkit.entity.Shulker
|
||||||
import org.bukkit.inventory.ItemStack
|
import org.bukkit.inventory.ItemStack
|
||||||
import org.bukkit.persistence.PersistentDataType
|
import org.bukkit.persistence.PersistentDataType
|
||||||
|
|
||||||
class LandSectorCommand(private val plugin: LandSectorPlugin) {
|
class LandSectorCommand(private val landSectorPlugin: LandSectorPlugin) {
|
||||||
fun register() {
|
fun register() {
|
||||||
kommand(plugin) {
|
kommand(landSectorPlugin) {
|
||||||
command("landsector") {
|
command("landsector") {
|
||||||
|
literal("givetool") {
|
||||||
|
executes {
|
||||||
|
val player = sender as? Player ?: return@executes
|
||||||
|
giveTool(player, null)
|
||||||
|
}
|
||||||
|
integer("sectorId") {
|
||||||
|
executes {
|
||||||
|
val player = sender as? Player ?: return@executes
|
||||||
|
val id = argument<Int>("sectorId")
|
||||||
|
giveTool(player, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
literal("activate") {
|
||||||
|
integer("sectorId") {
|
||||||
|
executes {
|
||||||
|
val id = argument<Int>("sectorId")
|
||||||
|
sender.sendMessage(Component.text("Activation for Sector #$id pending implementation.", NamedTextColor.YELLOW))
|
||||||
|
// TODO: Validate selection and lock in
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
literal("cancel") {
|
||||||
|
integer("sectorId") {
|
||||||
|
executes {
|
||||||
|
val id = argument<Int>("sectorId")
|
||||||
|
sender.sendMessage(Component.text("Cancellation for Sector #$id pending implementation.", NamedTextColor.YELLOW))
|
||||||
|
// TODO: Cancel logic
|
||||||
|
val player = sender as? Player
|
||||||
|
if (player != null) {
|
||||||
|
landSectorPlugin.selectionService?.clearSelection(player.uniqueId)
|
||||||
|
sender.sendMessage(Component.text("Selection cleared.", NamedTextColor.RED))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
literal("give") {
|
literal("give") {
|
||||||
executes {
|
executes {
|
||||||
val player = sender as? Player ?: return@executes
|
val player = sender as? Player ?: return@executes
|
||||||
|
|
@ -22,7 +65,7 @@ class LandSectorCommand(private val plugin: LandSectorPlugin) {
|
||||||
val meta = item.itemMeta
|
val meta = item.itemMeta
|
||||||
meta.displayName(Component.text("Sector Core", NamedTextColor.LIGHT_PURPLE))
|
meta.displayName(Component.text("Sector Core", NamedTextColor.LIGHT_PURPLE))
|
||||||
meta.persistentDataContainer.set(
|
meta.persistentDataContainer.set(
|
||||||
NamespacedKey(plugin, "component"),
|
NamespacedKey(landSectorPlugin, "component"),
|
||||||
PersistentDataType.STRING,
|
PersistentDataType.STRING,
|
||||||
"sector_core"
|
"sector_core"
|
||||||
)
|
)
|
||||||
|
|
@ -32,7 +75,124 @@ class LandSectorCommand(private val plugin: LandSectorPlugin) {
|
||||||
sender.sendMessage(Component.text("Gave 1 Sector Core.", NamedTextColor.GREEN))
|
sender.sendMessage(Component.text("Gave 1 Sector Core.", NamedTextColor.GREEN))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
literal("list") {
|
||||||
|
executes {
|
||||||
|
val player = sender as? Player ?: return@executes
|
||||||
|
val service = landSectorPlugin.sectorService
|
||||||
|
if (service == null) {
|
||||||
|
sender.sendMessage(Component.text("SectorService not ready.", NamedTextColor.RED))
|
||||||
|
return@executes
|
||||||
|
}
|
||||||
|
|
||||||
|
val worldName = player.world.name
|
||||||
|
val loc = player.location
|
||||||
|
|
||||||
|
val sectors = service.getAllSectors(worldName)
|
||||||
|
.sortedBy { sector ->
|
||||||
|
val sx = sector.x + 0.5
|
||||||
|
val sy = sector.y + 1.0
|
||||||
|
val sz = sector.z + 0.5
|
||||||
|
|
||||||
|
(loc.x - sx) * (loc.x - sx) +
|
||||||
|
(loc.y - sy) * (loc.y - sy) +
|
||||||
|
(loc.z - sz) * (loc.z - sz)
|
||||||
|
}
|
||||||
|
|
||||||
|
sender.sendMessage(Component.text("=== Sector List (${sectors.size}) ===", NamedTextColor.GOLD))
|
||||||
|
sectors.forEach { sector ->
|
||||||
|
val distSq = (loc.x - (sector.x + 0.5)) * (loc.x - (sector.x + 0.5)) +
|
||||||
|
(loc.y - (sector.y + 1.0)) * (loc.y - (sector.y + 1.0)) +
|
||||||
|
(loc.z - (sector.z + 0.5)) * (loc.z - (sector.z + 0.5))
|
||||||
|
val dist = Math.sqrt(distSq).toInt()
|
||||||
|
|
||||||
|
sender.sendMessage(
|
||||||
|
Component.text()
|
||||||
|
.append(Component.text("#${sector.id} ", NamedTextColor.YELLOW))
|
||||||
|
.append(Component.text("(${sector.x}, ${sector.y}, ${sector.z}) ", NamedTextColor.GRAY))
|
||||||
|
.append(Component.text("HP: ${sector.hp} ", NamedTextColor.RED))
|
||||||
|
.append(Component.text("- ${dist}m", NamedTextColor.AQUA))
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
literal("delete") {
|
||||||
|
integer("id") {
|
||||||
|
executes {
|
||||||
|
val id = argument<Int>("id")
|
||||||
|
val service = landSectorPlugin.sectorService
|
||||||
|
if (service == null) {
|
||||||
|
sender.sendMessage(Component.text("SectorService not ready.", NamedTextColor.RED))
|
||||||
|
return@executes
|
||||||
|
}
|
||||||
|
|
||||||
|
val sector = service.deleteSector(id)
|
||||||
|
if (sector == null) {
|
||||||
|
sender.sendMessage(Component.text("Sector #$id not found.", NamedTextColor.RED))
|
||||||
|
} else {
|
||||||
|
// Physical removal
|
||||||
|
val world = Bukkit.getWorld(sector.world)
|
||||||
|
if (world != null) {
|
||||||
|
val x = sector.x
|
||||||
|
val y = sector.y
|
||||||
|
val z = sector.z
|
||||||
|
|
||||||
|
val blockBase = world.getBlockAt(x, y - 1, z)
|
||||||
|
val blockTop = world.getBlockAt(x, y + 1, z)
|
||||||
|
|
||||||
|
if (blockBase.type == Material.DEEPSLATE_TILE_SLAB) blockBase.type = Material.AIR
|
||||||
|
if (blockTop.type == Material.DEEPSLATE_TILE_SLAB) blockTop.type = Material.AIR
|
||||||
|
|
||||||
|
// Remove All Entities with this sector ID
|
||||||
|
val center = Location(world, x + 0.5, y.toDouble(), z + 0.5)
|
||||||
|
if (center.chunk.isLoaded) {
|
||||||
|
val sectorKey = NamespacedKey(landSectorPlugin, "sector_id")
|
||||||
|
center.chunk.entities.forEach { entity ->
|
||||||
|
if (entity.persistentDataContainer.has(sectorKey, PersistentDataType.INTEGER)) {
|
||||||
|
val sId = entity.persistentDataContainer.get(sectorKey, PersistentDataType.INTEGER)
|
||||||
|
if (sId == id) {
|
||||||
|
entity.remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sender.sendMessage(Component.text("Sector #$id deleted.", NamedTextColor.GREEN))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun giveTool(player: Player, sectorId: Int?) {
|
||||||
|
val item = ItemStack(Material.FLINT)
|
||||||
|
val meta = item.itemMeta
|
||||||
|
meta.displayName(Component.text("Range Selection Tool" + (if (sectorId != null) " (#$sectorId)" else ""), NamedTextColor.AQUA))
|
||||||
|
meta.persistentDataContainer.set(
|
||||||
|
NamespacedKey(landSectorPlugin, "component"),
|
||||||
|
PersistentDataType.STRING,
|
||||||
|
"land_sector_tool"
|
||||||
|
)
|
||||||
|
if (sectorId != null) {
|
||||||
|
meta.persistentDataContainer.set(
|
||||||
|
NamespacedKey(landSectorPlugin, "sector_id"),
|
||||||
|
PersistentDataType.INTEGER,
|
||||||
|
sectorId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
meta.lore(listOf(
|
||||||
|
Component.text("Left Click: Switch Mode", NamedTextColor.GRAY),
|
||||||
|
Component.text("Right Click: Select Position", NamedTextColor.GRAY),
|
||||||
|
Component.text("Sneaking acts as modifier", NamedTextColor.DARK_GRAY)
|
||||||
|
) + if (sectorId != null) listOf(Component.text("Linked to Sector #$sectorId", NamedTextColor.GOLD)) else emptyList())
|
||||||
|
item.itemMeta = meta
|
||||||
|
|
||||||
|
player.inventory.addItem(item)
|
||||||
|
player.sendMessage(Component.text("Gave Range Selection Tool${if (sectorId != null) " for Sector #$sectorId" else ""}.", NamedTextColor.GREEN))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,22 @@ object SectorsTable : Table("land_sectors") {
|
||||||
val x = integer("x")
|
val x = integer("x")
|
||||||
val y = integer("y")
|
val y = integer("y")
|
||||||
val z = integer("z")
|
val z = integer("z")
|
||||||
|
val hp = integer("hp").default(1000)
|
||||||
|
|
||||||
|
override val primaryKey = PrimaryKey(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
object SectorRangesTable : Table("land_sector_ranges") {
|
||||||
|
val id = integer("id").autoIncrement()
|
||||||
|
val sectorId = integer("sector_id").references(SectorsTable.id)
|
||||||
|
val type = varchar("type", 16)
|
||||||
|
val x1 = integer("x1")
|
||||||
|
val y1 = integer("y1")
|
||||||
|
val z1 = integer("z1")
|
||||||
|
val x2 = integer("x2")
|
||||||
|
val y2 = integer("y2")
|
||||||
|
val z2 = integer("z2")
|
||||||
|
val isSneaking = bool("is_sneaking")
|
||||||
|
|
||||||
override val primaryKey = PrimaryKey(id)
|
override val primaryKey = PrimaryKey(id)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,22 +7,62 @@ import net.kyori.adventure.text.Component
|
||||||
import net.kyori.adventure.text.format.NamedTextColor
|
import net.kyori.adventure.text.format.NamedTextColor
|
||||||
import org.bukkit.Material
|
import org.bukkit.Material
|
||||||
import org.bukkit.NamespacedKey
|
import org.bukkit.NamespacedKey
|
||||||
|
import org.bukkit.Particle
|
||||||
|
import org.bukkit.Sound
|
||||||
import org.bukkit.entity.EntityType
|
import org.bukkit.entity.EntityType
|
||||||
import org.bukkit.entity.Shulker
|
import org.bukkit.entity.Shulker
|
||||||
import org.bukkit.attribute.Attribute
|
import org.bukkit.attribute.Attribute
|
||||||
import org.bukkit.event.EventHandler
|
import org.bukkit.event.EventHandler
|
||||||
import org.bukkit.event.Listener
|
import org.bukkit.event.Listener
|
||||||
|
import org.bukkit.event.block.BlockBreakEvent
|
||||||
import org.bukkit.event.block.BlockPlaceEvent
|
import org.bukkit.event.block.BlockPlaceEvent
|
||||||
|
import org.bukkit.event.entity.EntityDamageEvent
|
||||||
|
import org.bukkit.event.entity.EntityDamageByEntityEvent
|
||||||
|
import org.bukkit.event.world.ChunkLoadEvent
|
||||||
|
import org.bukkit.entity.Player
|
||||||
|
import org.bukkit.entity.BlockDisplay
|
||||||
import org.bukkit.persistence.PersistentDataType
|
import org.bukkit.persistence.PersistentDataType
|
||||||
|
import org.bukkit.util.Transformation
|
||||||
|
import org.joml.Vector3f
|
||||||
|
import org.joml.Quaternionf
|
||||||
|
import org.bukkit.block.data.type.Slab
|
||||||
|
|
||||||
|
import net.hareworks.hcu.landsector.service.SelectionService
|
||||||
|
import net.kyori.adventure.text.event.ClickEvent
|
||||||
|
import org.bukkit.event.player.PlayerInteractEntityEvent
|
||||||
|
|
||||||
class SectorListener(
|
class SectorListener(
|
||||||
private val plugin: LandSectorPlugin,
|
private val plugin: LandSectorPlugin,
|
||||||
private val sectorService: SectorService,
|
private val sectorService: SectorService,
|
||||||
private val playerIdService: PlayerIdService
|
private val playerIdService: PlayerIdService,
|
||||||
|
private val selectionService: SelectionService
|
||||||
) : Listener {
|
) : Listener {
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
fun onBreak(event: BlockBreakEvent) {
|
||||||
|
val block = event.block
|
||||||
|
val loc = block.location
|
||||||
|
val player = event.player
|
||||||
|
|
||||||
|
if (sectorService.isSectorBlock(loc.world.name, loc.blockX, loc.blockY, loc.blockZ)) {
|
||||||
|
player.sendMessage(Component.text("You cannot break Sector Core blocks!", NamedTextColor.RED))
|
||||||
|
event.isCancelled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@EventHandler
|
@EventHandler
|
||||||
fun onPlace(event: BlockPlaceEvent) {
|
fun onPlace(event: BlockPlaceEvent) {
|
||||||
|
val block = event.block
|
||||||
|
val loc = block.location
|
||||||
|
val player = event.player
|
||||||
|
|
||||||
|
// Global protection check
|
||||||
|
if (sectorService.isSectorArea(loc.world.name, loc.blockX, loc.blockY, loc.blockZ)) {
|
||||||
|
player.sendMessage(Component.text("This area is protected by a Sector Core.", NamedTextColor.RED))
|
||||||
|
event.isCancelled = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val item = event.itemInHand
|
val item = event.itemInHand
|
||||||
val meta = item.itemMeta ?: return
|
val meta = item.itemMeta ?: return
|
||||||
val key = NamespacedKey(plugin, "component")
|
val key = NamespacedKey(plugin, "component")
|
||||||
|
|
@ -31,7 +71,6 @@ class SectorListener(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val player = event.player
|
|
||||||
val playerEntry = playerIdService.find(player.uniqueId)
|
val playerEntry = playerIdService.find(player.uniqueId)
|
||||||
if (playerEntry == null) {
|
if (playerEntry == null) {
|
||||||
player.sendMessage(Component.text("Identity not found.", NamedTextColor.RED))
|
player.sendMessage(Component.text("Identity not found.", NamedTextColor.RED))
|
||||||
|
|
@ -39,42 +78,389 @@ class SectorListener(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val block = event.blockPlaced
|
// ... (rest of the code)
|
||||||
val loc = block.location
|
|
||||||
|
|
||||||
// Space check
|
// Check 3x3x3 space availability (relative to placed block base)
|
||||||
val above1 = block.getRelative(0, 1, 0)
|
val baseLoc = block.location
|
||||||
val above2 = block.getRelative(0, 2, 0)
|
for (dx in -1..1) {
|
||||||
|
for (dy in 0..2) {
|
||||||
|
for (dz in -1..1) {
|
||||||
|
if (dx == 0 && dy == 0 && dz == 0) continue // Skip the placed block itself
|
||||||
|
val checkLoc = baseLoc.clone().add(dx.toDouble(), dy.toDouble(), dz.toDouble())
|
||||||
|
if (!checkLoc.block.type.isAir) {
|
||||||
|
player.sendMessage(Component.text("Not enough space! Need 3x3x3 free area.", NamedTextColor.RED))
|
||||||
|
event.isCancelled = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!above1.type.isAir || !above2.type.isAir) {
|
// Define Center Location (This will be the DB coordinates)
|
||||||
player.sendMessage(Component.text("Not enough space for Sector Core.", NamedTextColor.RED))
|
val centerLoc = baseLoc.clone().add(0.0, 1.0, 0.0) // y+1 from base
|
||||||
|
|
||||||
|
val sector = sectorService.createSector(
|
||||||
|
playerEntry.actorId,
|
||||||
|
player.world.name,
|
||||||
|
centerLoc.blockX,
|
||||||
|
centerLoc.blockY,
|
||||||
|
centerLoc.blockZ
|
||||||
|
)
|
||||||
|
|
||||||
|
if (sector == null) {
|
||||||
|
player.sendMessage(Component.text("Failed to create sector.", NamedTextColor.RED))
|
||||||
event.isCancelled = true
|
event.isCancelled = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Spawn Shulker
|
// Create Visuals
|
||||||
val shulkerLoc = loc.clone().add(0.5, 1.0, 0.5)
|
val sectorKey = NamespacedKey(plugin, "sector_id")
|
||||||
|
val visualCenter = centerLoc.clone().add(0.5, 0.0, 0.5) // Center of the block space
|
||||||
|
|
||||||
|
// 1. Command Block (Center + 0.5y) -> Matches old base+1.5
|
||||||
|
val cbLoc = visualCenter.clone().add(0.0, 0.5, 0.0)
|
||||||
|
val cb = player.world.spawnEntity(cbLoc, EntityType.BLOCK_DISPLAY) as BlockDisplay
|
||||||
|
cb.block = Material.COMMAND_BLOCK.createBlockData {
|
||||||
|
(it as org.bukkit.block.data.Directional).facing = org.bukkit.block.BlockFace.UP
|
||||||
|
}
|
||||||
|
cb.transformation = Transformation(
|
||||||
|
Vector3f(-0.25f, -0.25f, -0.25f),
|
||||||
|
Quaternionf(0f, 0f, 0f, 1f),
|
||||||
|
Vector3f(0.5f, 0.5f, 0.5f),
|
||||||
|
Quaternionf(0f, 0f, 0f, 1f)
|
||||||
|
)
|
||||||
|
cb.persistentDataContainer.set(sectorKey, PersistentDataType.INTEGER, sector.id)
|
||||||
|
|
||||||
|
// 2. Tinted Glass (Center + 0.5y) -> Matches old base+1.5
|
||||||
|
val glass = player.world.spawnEntity(cbLoc, EntityType.BLOCK_DISPLAY) as BlockDisplay
|
||||||
|
glass.block = Material.TINTED_GLASS.createBlockData()
|
||||||
|
glass.transformation = Transformation(
|
||||||
|
Vector3f(-0.4375f, -0.4375f, -0.4375f),
|
||||||
|
Quaternionf(0f, 0f, 0f, 1f),
|
||||||
|
Vector3f(0.875f, 0.875f, 0.875f),
|
||||||
|
Quaternionf(0f, 0f, 0f, 1f)
|
||||||
|
)
|
||||||
|
glass.persistentDataContainer.set(sectorKey, PersistentDataType.INTEGER, sector.id)
|
||||||
|
|
||||||
|
// 3. Top Cauldron (Center + 1.3125y) -> Matches old base+2.3125
|
||||||
|
val topCauldronLoc = visualCenter.clone().add(0.0, 1.3125, 0.0)
|
||||||
|
val topCauldron = player.world.spawnEntity(topCauldronLoc, EntityType.BLOCK_DISPLAY) as BlockDisplay
|
||||||
|
topCauldron.block = Material.CAULDRON.createBlockData()
|
||||||
|
topCauldron.transformation = Transformation(
|
||||||
|
Vector3f(0.4990f, -0.4990f, 0.4990f),
|
||||||
|
Quaternionf(0f, 1f, 0f, 0f),
|
||||||
|
Vector3f(0.9980f, 0.9980f, 0.9980f),
|
||||||
|
Quaternionf(0f, 0f, 0f, 1f)
|
||||||
|
)
|
||||||
|
topCauldron.persistentDataContainer.set(sectorKey, PersistentDataType.INTEGER, sector.id)
|
||||||
|
|
||||||
|
// 4. Bottom Cauldron (Center - 0.3125y) -> Matches old base+0.6875
|
||||||
|
val botCauldronLoc = visualCenter.clone().add(0.0, -0.3125, 0.0)
|
||||||
|
val botCauldron = player.world.spawnEntity(botCauldronLoc, EntityType.BLOCK_DISPLAY) as BlockDisplay
|
||||||
|
botCauldron.block = Material.CAULDRON.createBlockData()
|
||||||
|
botCauldron.transformation = Transformation(
|
||||||
|
Vector3f(-0.4990f, 0.4990f, 0.4990f),
|
||||||
|
Quaternionf(1f, 0f, 0f, 0f),
|
||||||
|
Vector3f(0.9980f, 0.9980f, 0.9980f),
|
||||||
|
Quaternionf(0f, 0f, 0f, 1f)
|
||||||
|
)
|
||||||
|
botCauldron.persistentDataContainer.set(sectorKey, PersistentDataType.INTEGER, sector.id)
|
||||||
|
|
||||||
|
// Spawn Shulker (Hitbox) at Center
|
||||||
|
val shulkerLoc = visualCenter.clone() // Center is exactly y+1 from base
|
||||||
val shulker = player.world.spawnEntity(shulkerLoc, EntityType.SHULKER) as Shulker
|
val shulker = player.world.spawnEntity(shulkerLoc, EntityType.SHULKER) as Shulker
|
||||||
shulker.setAI(false)
|
shulker.setAI(false)
|
||||||
shulker.isInvulnerable = true
|
|
||||||
shulker.isInvisible = true
|
shulker.isInvisible = true
|
||||||
|
// shulker.isInvulnerable = true // REMOVED to allow damage
|
||||||
|
|
||||||
val param = shulker.getAttribute(Attribute.MAX_HEALTH)
|
val param = shulker.getAttribute(Attribute.MAX_HEALTH)
|
||||||
param?.baseValue = 1000.0
|
param?.baseValue = 1000.0
|
||||||
shulker.health = 1000.0
|
shulker.health = 1000.0
|
||||||
|
shulker.maximumNoDamageTicks = 0
|
||||||
|
|
||||||
// Place top bedrock
|
// Tag Shulker with Sector ID
|
||||||
above2.type = Material.BEDROCK
|
shulker.persistentDataContainer.set(sectorKey, PersistentDataType.INTEGER, sector.id)
|
||||||
|
|
||||||
// Record to DB (using Shulker pos as center)
|
// Place Blocks (Base and Top)
|
||||||
sectorService.createSector(
|
// Base is at center.y - 1 (The placed block)
|
||||||
playerEntry.actorId,
|
// Top is at center.y + 1
|
||||||
player.world.name,
|
|
||||||
above1.x,
|
block.type = Material.DEEPSLATE_TILE_SLAB
|
||||||
above1.y,
|
val bottomSlab = block.blockData as Slab
|
||||||
above1.z
|
bottomSlab.type = Slab.Type.BOTTOM
|
||||||
)
|
block.blockData = bottomSlab
|
||||||
|
|
||||||
|
val topBlock = centerLoc.clone().add(0.0, 1.0, 0.0).block
|
||||||
|
topBlock.type = Material.DEEPSLATE_TILE_SLAB
|
||||||
|
val topSlab = topBlock.blockData as Slab
|
||||||
|
topSlab.type = Slab.Type.TOP
|
||||||
|
topBlock.blockData = topSlab
|
||||||
|
|
||||||
player.sendMessage(Component.text("Sector Core placed!", NamedTextColor.GREEN))
|
player.sendMessage(Component.text("Sector Core placed!", NamedTextColor.GREEN))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
fun onInteractEntity(event: PlayerInteractEntityEvent) {
|
||||||
|
if (event.hand != org.bukkit.inventory.EquipmentSlot.HAND) return
|
||||||
|
val entity = event.rightClicked
|
||||||
|
if (entity !is Shulker) return
|
||||||
|
|
||||||
|
val sectorKey = NamespacedKey(plugin, "sector_id")
|
||||||
|
if (!entity.persistentDataContainer.has(sectorKey, PersistentDataType.INTEGER)) return
|
||||||
|
|
||||||
|
val sectorId = entity.persistentDataContainer.get(sectorKey, PersistentDataType.INTEGER) ?: return
|
||||||
|
val player = event.player
|
||||||
|
|
||||||
|
// Ownership Check
|
||||||
|
val pEntry = playerIdService.find(player.uniqueId)
|
||||||
|
if (pEntry == null) return
|
||||||
|
val sector = sectorService.getSector(sectorId) ?: return
|
||||||
|
|
||||||
|
if (sector.ownerActorId != pEntry.actorId) {
|
||||||
|
player.sendMessage(Component.text("You are not the owner of this sector.", NamedTextColor.RED))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open GUI
|
||||||
|
val gui = org.bukkit.Bukkit.createInventory(null, 27, Component.text("Sector Manager (#${sectorId})", NamedTextColor.BLACK))
|
||||||
|
|
||||||
|
// Info Item with Ranges
|
||||||
|
val ranges = sectorService.getRanges(sectorId)
|
||||||
|
val rangeLore = ranges.map { Component.text(" - [${it.id}] ${it.type}", NamedTextColor.GRAY) }
|
||||||
|
val infoLore = mutableListOf<Component>()
|
||||||
|
infoLore.add(Component.text("Sector ID: $sectorId", NamedTextColor.WHITE))
|
||||||
|
infoLore.addAll(rangeLore)
|
||||||
|
|
||||||
|
gui.setItem(4, createGuiItem(
|
||||||
|
Material.PAPER,
|
||||||
|
Component.text("Sector Info", NamedTextColor.GOLD),
|
||||||
|
infoLore,
|
||||||
|
sectorId,
|
||||||
|
"info"
|
||||||
|
))
|
||||||
|
|
||||||
|
// Get Selection Tool
|
||||||
|
gui.setItem(11, createGuiItem(
|
||||||
|
Material.FLINT,
|
||||||
|
Component.text("Get Selection Tool", NamedTextColor.AQUA),
|
||||||
|
listOf(Component.text("Click to receive the selection tool.", NamedTextColor.GRAY)),
|
||||||
|
sectorId,
|
||||||
|
"give_tool"
|
||||||
|
))
|
||||||
|
|
||||||
|
// Activate
|
||||||
|
gui.setItem(13, createGuiItem(
|
||||||
|
Material.LIME_CONCRETE,
|
||||||
|
Component.text("Activate Sector", NamedTextColor.GREEN),
|
||||||
|
listOf(Component.text("Click to activate this sector.", NamedTextColor.GRAY)),
|
||||||
|
sectorId,
|
||||||
|
"activate"
|
||||||
|
))
|
||||||
|
|
||||||
|
// Cancel / Destroy
|
||||||
|
gui.setItem(15, createGuiItem(
|
||||||
|
Material.RED_CONCRETE,
|
||||||
|
Component.text("Cancel / Destroy", NamedTextColor.RED),
|
||||||
|
listOf(Component.text("Click to destroy this sector.", NamedTextColor.GRAY)),
|
||||||
|
sectorId,
|
||||||
|
"cancel"
|
||||||
|
))
|
||||||
|
|
||||||
|
player.openInventory(gui)
|
||||||
|
|
||||||
|
event.isCancelled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
fun onDamage(event: EntityDamageEvent) {
|
||||||
|
val entity = event.entity
|
||||||
|
if (entity !is Shulker) return
|
||||||
|
|
||||||
|
val sectorKey = NamespacedKey(plugin, "sector_id")
|
||||||
|
if (!entity.persistentDataContainer.has(sectorKey, PersistentDataType.INTEGER)) return
|
||||||
|
|
||||||
|
val sectorId = entity.persistentDataContainer.get(sectorKey, PersistentDataType.INTEGER) ?: return
|
||||||
|
|
||||||
|
// Prevent vanilla death logic but show damage effect
|
||||||
|
// We restore health to max after processing
|
||||||
|
val damage = event.finalDamage
|
||||||
|
val maxHealth = entity.getAttribute(Attribute.MAX_HEALTH)?.value ?: 1000.0
|
||||||
|
|
||||||
|
// Cancel the event so it doesn't actually die in vanilla terms,
|
||||||
|
// OR let it happen but force health reset.
|
||||||
|
// Cancelling prevents the red flash in some versions/cases, so setting damage to 0 is often better
|
||||||
|
// if we want the effect. But resetting health is robust.
|
||||||
|
val oldHp = sectorService.getHealth(sectorId) ?: return // Get current health from service
|
||||||
|
val newHp = sectorService.reduceHealth(sectorId, damage.toInt())
|
||||||
|
|
||||||
|
entity.health = maxHealth
|
||||||
|
entity.noDamageTicks = 0 // Disable invulnerability
|
||||||
|
|
||||||
|
if (newHp != null) {
|
||||||
|
val loc = entity.location
|
||||||
|
val world = entity.world
|
||||||
|
|
||||||
|
// Sound Logic
|
||||||
|
val threshold = 100 // 10% of 1000
|
||||||
|
val crossedThreshold = (oldHp / threshold) > (newHp / threshold)
|
||||||
|
|
||||||
|
if (crossedThreshold) {
|
||||||
|
world.playSound(loc, Sound.BLOCK_ANVIL_PLACE, 1.0f, 0.5f)
|
||||||
|
} else {
|
||||||
|
world.playSound(loc, Sound.BLOCK_STONE_BREAK, 1.0f, 1.0f)
|
||||||
|
}
|
||||||
|
|
||||||
|
world.spawnParticle(Particle.BLOCK, loc.add(0.0, 0.5, 0.0), 20, 0.3, 0.3, 0.3, Material.BEDROCK.createBlockData())
|
||||||
|
|
||||||
|
if (event is EntityDamageByEntityEvent && event.damager is Player) {
|
||||||
|
val player = event.damager as Player
|
||||||
|
|
||||||
|
val percent = newHp.toDouble() / maxHealth
|
||||||
|
val color = when {
|
||||||
|
percent > 0.5 -> NamedTextColor.GREEN
|
||||||
|
percent > 0.2 -> NamedTextColor.YELLOW
|
||||||
|
else -> NamedTextColor.RED
|
||||||
|
}
|
||||||
|
|
||||||
|
val progressBar = createProgressBar(newHp, maxHealth.toInt(), color)
|
||||||
|
|
||||||
|
val message = Component.text()
|
||||||
|
.append(Component.text("Sector Core ", NamedTextColor.GOLD))
|
||||||
|
.append(progressBar)
|
||||||
|
.append(Component.text(" "))
|
||||||
|
.append(Component.text(newHp, color))
|
||||||
|
.append(Component.text("/", NamedTextColor.GRAY))
|
||||||
|
.append(Component.text(maxHealth.toInt(), NamedTextColor.GRAY))
|
||||||
|
.build()
|
||||||
|
|
||||||
|
player.sendActionBar(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newHp <= 0) {
|
||||||
|
// Destruction
|
||||||
|
entity.remove()
|
||||||
|
|
||||||
|
// Remove other visuals in chunk
|
||||||
|
val chunk = entity.chunk
|
||||||
|
chunk.entities.forEach { ent ->
|
||||||
|
val sId = ent.persistentDataContainer.get(sectorKey, PersistentDataType.INTEGER)
|
||||||
|
if (sId == sectorId) ent.remove()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove blocks.
|
||||||
|
val x = loc.blockX
|
||||||
|
val y = loc.blockY
|
||||||
|
val z = loc.blockZ
|
||||||
|
|
||||||
|
val base = world.getBlockAt(x, y - 1, z)
|
||||||
|
val top = world.getBlockAt(x, y + 1, z)
|
||||||
|
|
||||||
|
if (base.type == Material.DEEPSLATE_TILE_SLAB) base.type = Material.AIR
|
||||||
|
if (top.type == Material.DEEPSLATE_TILE_SLAB) top.type = Material.AIR
|
||||||
|
|
||||||
|
world.dropItemNaturally(loc, org.bukkit.inventory.ItemStack(Material.DEEPSLATE_TILE_SLAB, 2))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createProgressBar(current: Int, max: Int, color: NamedTextColor): Component {
|
||||||
|
val totalBars = 20
|
||||||
|
val percent = current.toDouble() / max.toDouble()
|
||||||
|
val filledBars = (totalBars * percent).toInt().coerceIn(0, totalBars)
|
||||||
|
val emptyBars = totalBars - filledBars
|
||||||
|
|
||||||
|
return Component.text()
|
||||||
|
.append(Component.text("[", NamedTextColor.DARK_GRAY))
|
||||||
|
.append(Component.text("|".repeat(filledBars), color))
|
||||||
|
.append(Component.text("|".repeat(emptyBars), NamedTextColor.GRAY))
|
||||||
|
.append(Component.text("]", NamedTextColor.DARK_GRAY))
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
fun onChunkLoad(event: ChunkLoadEvent) {
|
||||||
|
if (event.isNewChunk) return // Optimization: New chunks won't have old sectors to cleanup
|
||||||
|
|
||||||
|
val chunk = event.chunk
|
||||||
|
val entities = chunk.entities
|
||||||
|
val sectorKey = NamespacedKey(plugin, "sector_id")
|
||||||
|
|
||||||
|
entities.forEach { entity ->
|
||||||
|
if (entity.persistentDataContainer.has(sectorKey, PersistentDataType.INTEGER)) {
|
||||||
|
val sectorId = entity.persistentDataContainer.get(sectorKey, PersistentDataType.INTEGER) ?: return@forEach
|
||||||
|
|
||||||
|
// Check existance async
|
||||||
|
plugin.server.scheduler.runTaskAsynchronously(plugin, Runnable {
|
||||||
|
val exists = sectorService.exists(sectorId)
|
||||||
|
if (!exists) {
|
||||||
|
plugin.server.scheduler.runTask(plugin, Runnable {
|
||||||
|
// Re-verify ent validity
|
||||||
|
if (entity.isValid) {
|
||||||
|
entity.remove()
|
||||||
|
// We should also clean up blocks, but that requires knowing where they are relative to entity
|
||||||
|
val loc = entity.location
|
||||||
|
val world = entity.world
|
||||||
|
val x = loc.blockX
|
||||||
|
val y = loc.blockY
|
||||||
|
val z = loc.blockZ
|
||||||
|
|
||||||
|
// Entity is at x, y, z
|
||||||
|
// Base is y-1, Top is y+1
|
||||||
|
val base = world.getBlockAt(x, y - 1, z)
|
||||||
|
val top = world.getBlockAt(x, y + 1, z)
|
||||||
|
|
||||||
|
if (base.type == Material.DEEPSLATE_TILE_SLAB) base.type = Material.AIR
|
||||||
|
if (top.type == Material.DEEPSLATE_TILE_SLAB) top.type = Material.AIR
|
||||||
|
|
||||||
|
plugin.logger.info("Removed orphaned sector artifacts for ID $sectorId at $x,$y,$z")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
fun onInventoryClick(event: org.bukkit.event.inventory.InventoryClickEvent) {
|
||||||
|
val item = event.currentItem ?: return
|
||||||
|
val meta = item.itemMeta ?: return
|
||||||
|
val pluginKey = NamespacedKey(plugin, "gui_action")
|
||||||
|
|
||||||
|
if (!meta.persistentDataContainer.has(pluginKey, PersistentDataType.STRING)) return
|
||||||
|
|
||||||
|
event.isCancelled = true // Prevent taking items
|
||||||
|
|
||||||
|
val player = event.whoClicked as? Player ?: return
|
||||||
|
val action = meta.persistentDataContainer.get(pluginKey, PersistentDataType.STRING)
|
||||||
|
val sectorKey = NamespacedKey(plugin, "sector_id")
|
||||||
|
val sectorId = meta.persistentDataContainer.get(sectorKey, PersistentDataType.INTEGER) ?: return
|
||||||
|
|
||||||
|
player.closeInventory()
|
||||||
|
|
||||||
|
when (action) {
|
||||||
|
"give_tool" -> player.performCommand("landsector givetool $sectorId")
|
||||||
|
"activate" -> player.performCommand("landsector activate $sectorId")
|
||||||
|
"cancel" -> player.performCommand("landsector cancel $sectorId")
|
||||||
|
"info" -> {
|
||||||
|
player.playSound(player.location, Sound.UI_BUTTON_CLICK, 1f, 1f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createGuiItem(material: Material, name: Component, lore: List<Component>, sectorId: Int, action: String): org.bukkit.inventory.ItemStack {
|
||||||
|
val item = org.bukkit.inventory.ItemStack(material)
|
||||||
|
val meta = item.itemMeta
|
||||||
|
meta.displayName(name)
|
||||||
|
meta.lore(lore)
|
||||||
|
|
||||||
|
val actionKey = NamespacedKey(plugin, "gui_action")
|
||||||
|
val sectorKey = NamespacedKey(plugin, "sector_id")
|
||||||
|
|
||||||
|
meta.persistentDataContainer.set(actionKey, PersistentDataType.STRING, action)
|
||||||
|
meta.persistentDataContainer.set(sectorKey, PersistentDataType.INTEGER, sectorId)
|
||||||
|
|
||||||
|
item.itemMeta = meta
|
||||||
|
return item
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,188 @@
|
||||||
|
package net.hareworks.hcu.landsector.listener
|
||||||
|
|
||||||
|
import net.hareworks.hcu.landsector.LandSectorPlugin
|
||||||
|
import net.hareworks.hcu.landsector.service.SelectionService
|
||||||
|
import net.hareworks.hcu.landsector.service.SectorService
|
||||||
|
import net.hareworks.hcu.landsector.model.SelectionMode
|
||||||
|
import net.kyori.adventure.text.Component
|
||||||
|
import net.kyori.adventure.text.format.NamedTextColor
|
||||||
|
import org.bukkit.Material
|
||||||
|
import org.bukkit.NamespacedKey
|
||||||
|
import org.bukkit.event.EventHandler
|
||||||
|
import org.bukkit.event.Listener
|
||||||
|
import org.bukkit.event.block.Action
|
||||||
|
import org.bukkit.event.player.PlayerInteractEvent
|
||||||
|
import org.bukkit.persistence.PersistentDataType
|
||||||
|
import org.bukkit.inventory.EquipmentSlot
|
||||||
|
|
||||||
|
class SelectionListener(
|
||||||
|
private val plugin: LandSectorPlugin,
|
||||||
|
private val selectionService: SelectionService,
|
||||||
|
private val sectorService: SectorService
|
||||||
|
) : Listener {
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
fun onInteract(event: PlayerInteractEvent) {
|
||||||
|
val item = event.item ?: return
|
||||||
|
if (item.type != Material.FLINT) return
|
||||||
|
|
||||||
|
val key = NamespacedKey(plugin, "component")
|
||||||
|
if (item.itemMeta?.persistentDataContainer?.get(key, PersistentDataType.STRING) != "land_sector_tool") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only Main Hand to avoid double fire
|
||||||
|
if (event.hand != EquipmentSlot.HAND) return
|
||||||
|
|
||||||
|
val player = event.player
|
||||||
|
val selection = selectionService.getSelection(player.uniqueId)
|
||||||
|
|
||||||
|
// Left Click: Switch Mode
|
||||||
|
if (event.action == Action.LEFT_CLICK_AIR || event.action == Action.LEFT_CLICK_BLOCK) {
|
||||||
|
event.isCancelled = true // Prevent breaking block
|
||||||
|
|
||||||
|
selection.mode = if (selection.mode == SelectionMode.CUBOID) SelectionMode.CYLINDER else SelectionMode.CUBOID
|
||||||
|
player.sendMessage(Component.text("Mode switched to: ${selection.mode}", NamedTextColor.YELLOW))
|
||||||
|
|
||||||
|
// Reset selection on mode switch
|
||||||
|
selection.point1 = null
|
||||||
|
selection.point2 = null
|
||||||
|
selection.p1Sneaking = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Right Click: Set Points
|
||||||
|
if (event.action == Action.RIGHT_CLICK_BLOCK) {
|
||||||
|
event.isCancelled = true // Prevent placing
|
||||||
|
|
||||||
|
val clickedBlock = event.clickedBlock ?: return
|
||||||
|
val loc = clickedBlock.location
|
||||||
|
|
||||||
|
if (selection.point1 == null) {
|
||||||
|
// Set Point 1
|
||||||
|
selection.point1 = loc
|
||||||
|
selection.p1Sneaking = player.isSneaking
|
||||||
|
selection.point2 = null // Clear p2 just in case
|
||||||
|
|
||||||
|
player.sendMessage(Component.text("Position 1 set at ${loc.blockX}, ${loc.blockY}, ${loc.blockZ} (Mode: ${selection.mode}, Sneaking: ${selection.p1Sneaking})", NamedTextColor.GREEN))
|
||||||
|
} else {
|
||||||
|
// Set Point 2
|
||||||
|
// If P2 is already set, reset to P1 new? standard standard is cyclic.
|
||||||
|
if (selection.point2 != null) {
|
||||||
|
// Resetting, treat as P1
|
||||||
|
selection.point1 = loc
|
||||||
|
selection.p1Sneaking = player.isSneaking
|
||||||
|
selection.point2 = null
|
||||||
|
player.sendMessage(Component.text("Selection reset. Position 1 set at ${loc.blockX}, ${loc.blockY}, ${loc.blockZ} (Mode: ${selection.mode}, Sneaking: ${selection.p1Sneaking})", NamedTextColor.GREEN))
|
||||||
|
} else {
|
||||||
|
selection.point2 = loc
|
||||||
|
// Check for sector ID
|
||||||
|
val itemStack = event.item
|
||||||
|
val sectorKey = NamespacedKey(plugin, "sector_id")
|
||||||
|
if (itemStack != null && itemStack.itemMeta.persistentDataContainer.has(sectorKey, PersistentDataType.INTEGER)) {
|
||||||
|
val sId = itemStack.itemMeta.persistentDataContainer.get(sectorKey, PersistentDataType.INTEGER)!!
|
||||||
|
val p1 = selection.point1!!
|
||||||
|
val p2 = selection.point2!!
|
||||||
|
|
||||||
|
// Add Range
|
||||||
|
val range = sectorService.addRange(
|
||||||
|
sId,
|
||||||
|
selection.mode,
|
||||||
|
p1.blockX, p1.blockY, p1.blockZ,
|
||||||
|
p2.blockX, p2.blockY, p2.blockZ,
|
||||||
|
selection.p1Sneaking
|
||||||
|
)
|
||||||
|
|
||||||
|
player.sendMessage(Component.text("Range added to Sector #$sId.", NamedTextColor.GREEN))
|
||||||
|
|
||||||
|
// Clear selection
|
||||||
|
selection.point1 = null
|
||||||
|
selection.point2 = null
|
||||||
|
} else {
|
||||||
|
player.sendMessage(Component.text("Position 2 set at ${loc.blockX}, ${loc.blockY}, ${loc.blockZ}", NamedTextColor.GREEN))
|
||||||
|
|
||||||
|
// Show Cost / Area
|
||||||
|
player.sendMessage(Component.text(selectionService.getAreaDetails(player.uniqueId), NamedTextColor.AQUA))
|
||||||
|
player.sendMessage(Component.text("Cost: ${selectionService.getCost(player.uniqueId)}", NamedTextColor.GOLD))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
fun onDrop(event: org.bukkit.event.player.PlayerDropItemEvent) {
|
||||||
|
val item = event.itemDrop.itemStack
|
||||||
|
val key = NamespacedKey(plugin, "component")
|
||||||
|
if (item.itemMeta?.persistentDataContainer?.get(key, PersistentDataType.STRING) == "land_sector_tool") {
|
||||||
|
event.isCancelled = true
|
||||||
|
event.player.sendMessage(Component.text("You cannot drop this tool.", NamedTextColor.RED))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
fun onInventoryClick(event: org.bukkit.event.inventory.InventoryClickEvent) {
|
||||||
|
val player = event.whoClicked as? org.bukkit.entity.Player ?: return
|
||||||
|
val clickedInv = event.clickedInventory
|
||||||
|
val action = event.action
|
||||||
|
|
||||||
|
fun isTool(stack: org.bukkit.inventory.ItemStack?): Boolean {
|
||||||
|
if (stack == null || stack.type != Material.FLINT) return false
|
||||||
|
val key = NamespacedKey(plugin, "component")
|
||||||
|
return stack.itemMeta?.persistentDataContainer?.get(key, PersistentDataType.STRING) == "land_sector_tool"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Prevent dropping via inventory
|
||||||
|
if (action.name.startsWith("DROP")) {
|
||||||
|
if (isTool(event.currentItem) || isTool(event.cursor)) {
|
||||||
|
event.isCancelled = true
|
||||||
|
player.sendMessage(Component.text("You cannot drop this tool.", NamedTextColor.RED))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Prevent interaction with tool in external inventories (Top Inventory)
|
||||||
|
// If clicking inside an inventory that is NOT the player's inventory
|
||||||
|
if (clickedInv != null && clickedInv != player.inventory) {
|
||||||
|
// If trying to place tool (Cursor has tool)
|
||||||
|
if (isTool(event.cursor)) {
|
||||||
|
event.isCancelled = true
|
||||||
|
player.sendMessage(Component.text("You cannot store this tool.", NamedTextColor.RED))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If trying to swap tool from hotbar (Number key)
|
||||||
|
if (event.click == org.bukkit.event.inventory.ClickType.NUMBER_KEY) {
|
||||||
|
val swappedItem = player.inventory.getItem(event.hotbarButton)
|
||||||
|
if (isTool(swappedItem)) {
|
||||||
|
event.isCancelled = true
|
||||||
|
player.sendMessage(Component.text("You cannot store this tool.", NamedTextColor.RED))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If trying to take tool IS in external inventory? (Shouldn't happen, but prevent taking just in case)
|
||||||
|
// This safeguards if somehow it got there.
|
||||||
|
if (isTool(event.currentItem)) {
|
||||||
|
event.isCancelled = true
|
||||||
|
// Allow them to break the item? No, just block interaction.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Prevent Shift-Clicking tool FROM player inventory TO external inventory
|
||||||
|
if (clickedInv == player.inventory && event.isShiftClick) {
|
||||||
|
if (isTool(event.currentItem)) {
|
||||||
|
// If top inventory is NOT Crafting/Creative (Personal), block transfer
|
||||||
|
val topType = event.view.topInventory.type
|
||||||
|
if (topType != org.bukkit.event.inventory.InventoryType.CRAFTING &&
|
||||||
|
topType != org.bukkit.event.inventory.InventoryType.CREATIVE) {
|
||||||
|
|
||||||
|
event.isCancelled = true
|
||||||
|
player.sendMessage(Component.text("You cannot store this tool.", NamedTextColor.RED))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,5 +6,6 @@ data class Sector(
|
||||||
val world: String,
|
val world: String,
|
||||||
val x: Int,
|
val x: Int,
|
||||||
val y: Int,
|
val y: Int,
|
||||||
val z: Int
|
val z: Int,
|
||||||
|
val hp: Int
|
||||||
)
|
)
|
||||||
|
|
|
||||||
14
bin/main/net/hareworks/hcu/landsector/model/SectorRange.kt
Normal file
14
bin/main/net/hareworks/hcu/landsector/model/SectorRange.kt
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
package net.hareworks.hcu.landsector.model
|
||||||
|
|
||||||
|
data class SectorRange(
|
||||||
|
val id: Int,
|
||||||
|
val sectorId: Int,
|
||||||
|
val type: SelectionMode,
|
||||||
|
val x1: Int,
|
||||||
|
val y1: Int,
|
||||||
|
val z1: Int,
|
||||||
|
val x2: Int,
|
||||||
|
val y2: Int,
|
||||||
|
val z2: Int,
|
||||||
|
val isSneaking: Boolean
|
||||||
|
)
|
||||||
15
bin/main/net/hareworks/hcu/landsector/model/SelectionData.kt
Normal file
15
bin/main/net/hareworks/hcu/landsector/model/SelectionData.kt
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
package net.hareworks.hcu.landsector.model
|
||||||
|
|
||||||
|
import org.bukkit.Location
|
||||||
|
|
||||||
|
enum class SelectionMode {
|
||||||
|
CUBOID,
|
||||||
|
CYLINDER
|
||||||
|
}
|
||||||
|
|
||||||
|
data class SelectionData(
|
||||||
|
var mode: SelectionMode = SelectionMode.CUBOID,
|
||||||
|
var point1: Location? = null,
|
||||||
|
var point2: Location? = null,
|
||||||
|
var p1Sneaking: Boolean = false
|
||||||
|
)
|
||||||
|
|
@ -1,20 +1,44 @@
|
||||||
package net.hareworks.hcu.landsector.service
|
package net.hareworks.hcu.landsector.service
|
||||||
|
|
||||||
import net.hareworks.hcu.landsector.database.SectorsTable
|
import net.hareworks.hcu.landsector.database.SectorsTable
|
||||||
|
import net.hareworks.hcu.landsector.database.SectorRangesTable
|
||||||
import net.hareworks.hcu.landsector.model.Sector
|
import net.hareworks.hcu.landsector.model.Sector
|
||||||
|
import net.hareworks.hcu.landsector.model.SectorRange
|
||||||
|
import net.hareworks.hcu.landsector.model.SelectionMode
|
||||||
import org.jetbrains.exposed.v1.core.eq
|
import org.jetbrains.exposed.v1.core.eq
|
||||||
import org.jetbrains.exposed.v1.jdbc.Database
|
import org.jetbrains.exposed.v1.jdbc.Database
|
||||||
import org.jetbrains.exposed.v1.jdbc.SchemaUtils
|
import org.jetbrains.exposed.v1.jdbc.SchemaUtils
|
||||||
import org.jetbrains.exposed.v1.jdbc.insert
|
import org.jetbrains.exposed.v1.jdbc.insert
|
||||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||||
|
import org.jetbrains.exposed.v1.jdbc.select
|
||||||
|
import org.jetbrains.exposed.v1.jdbc.update
|
||||||
import org.jetbrains.exposed.v1.jdbc.andWhere
|
import org.jetbrains.exposed.v1.jdbc.andWhere
|
||||||
|
import org.jetbrains.exposed.v1.jdbc.deleteWhere
|
||||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
class SectorService(private val database: Database) {
|
class SectorService(private val database: Database) {
|
||||||
|
|
||||||
|
private val hpCache = ConcurrentHashMap<Int, Int>()
|
||||||
|
private val dirtySet = ConcurrentHashMap.newKeySet<Int>()
|
||||||
|
private val sectorsCache = ConcurrentHashMap<Int, Sector>()
|
||||||
|
|
||||||
fun init() {
|
fun init() {
|
||||||
transaction(database) {
|
transaction(database) {
|
||||||
SchemaUtils.createMissingTablesAndColumns(SectorsTable)
|
SchemaUtils.createMissingTablesAndColumns(SectorsTable, SectorRangesTable)
|
||||||
|
SectorsTable.selectAll().forEach {
|
||||||
|
val id = it[SectorsTable.id]
|
||||||
|
val sector = Sector(
|
||||||
|
id,
|
||||||
|
it[SectorsTable.ownerActorId],
|
||||||
|
it[SectorsTable.world],
|
||||||
|
it[SectorsTable.x],
|
||||||
|
it[SectorsTable.y],
|
||||||
|
it[SectorsTable.z],
|
||||||
|
it[SectorsTable.hp]
|
||||||
|
)
|
||||||
|
sectorsCache[id] = sector
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -26,29 +50,223 @@ class SectorService(private val database: Database) {
|
||||||
it[SectorsTable.x] = x
|
it[SectorsTable.x] = x
|
||||||
it[SectorsTable.y] = y
|
it[SectorsTable.y] = y
|
||||||
it[SectorsTable.z] = z
|
it[SectorsTable.z] = z
|
||||||
|
it[SectorsTable.hp] = 1000
|
||||||
}[SectorsTable.id]
|
}[SectorsTable.id]
|
||||||
|
|
||||||
Sector(id, ownerActorId, world, x, y, z)
|
hpCache[id] = 1000
|
||||||
|
val sector = Sector(id, ownerActorId, world, x, y, z, 1000)
|
||||||
|
sectorsCache[id] = sector
|
||||||
|
sector
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSectorAt(world: String, x: Int, y: Int, z: Int): Sector? {
|
fun getSectorAt(world: String, x: Int, y: Int, z: Int): Sector? {
|
||||||
return transaction(database) {
|
// Optimized to use cache
|
||||||
SectorsTable.selectAll()
|
return sectorsCache.values.firstOrNull {
|
||||||
.andWhere { SectorsTable.world eq world }
|
it.world == world && it.x == x && it.y == y && it.z == z
|
||||||
.andWhere { SectorsTable.x eq x }
|
}
|
||||||
.andWhere { SectorsTable.y eq y }
|
}
|
||||||
.andWhere { SectorsTable.z eq z }
|
|
||||||
.map {
|
fun getSector(id: Int): Sector? {
|
||||||
|
return sectorsCache[id] ?: transaction(database) {
|
||||||
|
SectorsTable.selectAll().andWhere { SectorsTable.id eq id }.singleOrNull()?.let {
|
||||||
Sector(
|
Sector(
|
||||||
it[SectorsTable.id],
|
it[SectorsTable.id],
|
||||||
it[SectorsTable.ownerActorId],
|
it[SectorsTable.ownerActorId],
|
||||||
it[SectorsTable.world],
|
it[SectorsTable.world],
|
||||||
it[SectorsTable.x],
|
it[SectorsTable.x],
|
||||||
it[SectorsTable.y],
|
it[SectorsTable.y],
|
||||||
it[SectorsTable.z]
|
it[SectorsTable.z],
|
||||||
|
it[SectorsTable.hp]
|
||||||
)
|
)
|
||||||
}.singleOrNull()
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getHealth(id: Int): Int? {
|
||||||
|
// Try get from cache
|
||||||
|
var currentHp = hpCache[id]
|
||||||
|
|
||||||
|
// If not in cache, load from DB
|
||||||
|
if (currentHp == null) {
|
||||||
|
val sector = sectorsCache[id] ?: return null
|
||||||
|
|
||||||
|
currentHp = sector.hp
|
||||||
|
hpCache[id] = currentHp
|
||||||
|
}
|
||||||
|
return currentHp
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reduceHealth(id: Int, amount: Int): Int? {
|
||||||
|
// Try get from cache
|
||||||
|
var currentHp = hpCache[id]
|
||||||
|
|
||||||
|
// If not in cache, try to init from sectorsCache
|
||||||
|
if (currentHp == null) {
|
||||||
|
val sector = sectorsCache[id] ?: return null
|
||||||
|
currentHp = sector.hp
|
||||||
|
hpCache[id] = currentHp
|
||||||
|
}
|
||||||
|
|
||||||
|
val newHp = (currentHp!! - amount).coerceAtLeast(0)
|
||||||
|
hpCache[id] = newHp
|
||||||
|
dirtySet.add(id)
|
||||||
|
|
||||||
|
return newHp
|
||||||
|
}
|
||||||
|
|
||||||
|
fun flushChanges() {
|
||||||
|
if (dirtySet.isEmpty()) return
|
||||||
|
|
||||||
|
// Take a snapshot of keys to update
|
||||||
|
val toUpdate = dirtySet.toMutableSet()
|
||||||
|
dirtySet.removeAll(toUpdate) // Clear them from dirty set so we don't re-save unless modified again
|
||||||
|
|
||||||
|
if (toUpdate.isEmpty()) return
|
||||||
|
|
||||||
|
transaction(database) {
|
||||||
|
toUpdate.forEach { id ->
|
||||||
|
val hp = hpCache[id] ?: return@forEach
|
||||||
|
SectorsTable.update({ SectorsTable.id eq id }) {
|
||||||
|
it[SectorsTable.hp] = hp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteSector(id: Int): Sector? {
|
||||||
|
val sector = transaction(database) {
|
||||||
|
val record = SectorsTable.selectAll().andWhere { SectorsTable.id eq id }.singleOrNull() ?: return@transaction null
|
||||||
|
|
||||||
|
SectorRangesTable.deleteWhere { SectorRangesTable.sectorId eq id }
|
||||||
|
SectorsTable.deleteWhere { SectorsTable.id eq id }
|
||||||
|
|
||||||
|
Sector(
|
||||||
|
record[SectorsTable.id],
|
||||||
|
record[SectorsTable.ownerActorId],
|
||||||
|
record[SectorsTable.world],
|
||||||
|
record[SectorsTable.x],
|
||||||
|
record[SectorsTable.y],
|
||||||
|
record[SectorsTable.z],
|
||||||
|
record[SectorsTable.hp]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sector != null) {
|
||||||
|
hpCache.remove(id)
|
||||||
|
dirtySet.remove(id)
|
||||||
|
sectorsCache.remove(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sector
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isSectorBlock(world: String, x: Int, y: Int, z: Int): Boolean {
|
||||||
|
// Sector blocks are at center.y - 1 (Base) and center.y + 1 (Top)
|
||||||
|
// Center is at sector.y
|
||||||
|
return sectorsCache.values.any {
|
||||||
|
it.world == world &&
|
||||||
|
it.x == x &&
|
||||||
|
it.z == z &&
|
||||||
|
(it.y - 1 == y || it.y + 1 == y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isSectorArea(world: String, x: Int, y: Int, z: Int): Boolean {
|
||||||
|
// Check 3x3x3 around center (sector.x, sector.y, sector.z)
|
||||||
|
return sectorsCache.values.any {
|
||||||
|
it.world == world &&
|
||||||
|
x >= it.x - 1 && x <= it.x + 1 &&
|
||||||
|
y >= it.y - 1 && y <= it.y + 1 &&
|
||||||
|
z >= it.z - 1 && z <= it.z + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun exists(id: Int): Boolean {
|
||||||
|
if (hpCache.containsKey(id)) return true
|
||||||
|
|
||||||
|
return transaction(database) {
|
||||||
|
SectorsTable.selectAll().andWhere { SectorsTable.id eq id }.count() > 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAllSectorsList(): List<Sector> {
|
||||||
|
return transaction(database) {
|
||||||
|
SectorsTable.selectAll().map {
|
||||||
|
val id = it[SectorsTable.id]
|
||||||
|
val cachedHp = hpCache[id]
|
||||||
|
val hp = cachedHp ?: it[SectorsTable.hp]
|
||||||
|
Sector(
|
||||||
|
id,
|
||||||
|
it[SectorsTable.ownerActorId],
|
||||||
|
it[SectorsTable.world],
|
||||||
|
it[SectorsTable.x],
|
||||||
|
it[SectorsTable.y],
|
||||||
|
it[SectorsTable.z],
|
||||||
|
hp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAllSectors(world: String): List<Sector> {
|
||||||
|
return transaction(database) {
|
||||||
|
SectorsTable.selectAll()
|
||||||
|
.andWhere { SectorsTable.world eq world }
|
||||||
|
.map {
|
||||||
|
// Update HP from cache if exists
|
||||||
|
val id = it[SectorsTable.id]
|
||||||
|
val cachedHp = hpCache[id]
|
||||||
|
val hp = cachedHp ?: it[SectorsTable.hp]
|
||||||
|
|
||||||
|
Sector(
|
||||||
|
id,
|
||||||
|
it[SectorsTable.ownerActorId],
|
||||||
|
it[SectorsTable.world],
|
||||||
|
it[SectorsTable.x],
|
||||||
|
it[SectorsTable.y],
|
||||||
|
it[SectorsTable.z],
|
||||||
|
hp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun addRange(sectorId: Int, mode: SelectionMode, x1: Int, y1: Int, z1: Int, x2: Int, y2: Int, z2: Int, isSneaking: Boolean): SectorRange {
|
||||||
|
return transaction(database) {
|
||||||
|
val id = SectorRangesTable.insert {
|
||||||
|
it[SectorRangesTable.sectorId] = sectorId
|
||||||
|
it[SectorRangesTable.type] = mode.name
|
||||||
|
it[SectorRangesTable.x1] = x1
|
||||||
|
it[SectorRangesTable.y1] = y1
|
||||||
|
it[SectorRangesTable.z1] = z1
|
||||||
|
it[SectorRangesTable.x2] = x2
|
||||||
|
it[SectorRangesTable.y2] = y2
|
||||||
|
it[SectorRangesTable.z2] = z2
|
||||||
|
it[SectorRangesTable.isSneaking] = isSneaking
|
||||||
|
}[SectorRangesTable.id]
|
||||||
|
|
||||||
|
SectorRange(id, sectorId, mode, x1, y1, z1, x2, y2, z2, isSneaking)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getRanges(sectorId: Int): List<SectorRange> {
|
||||||
|
return transaction(database) {
|
||||||
|
SectorRangesTable.selectAll().andWhere { SectorRangesTable.sectorId eq sectorId }.map {
|
||||||
|
SectorRange(
|
||||||
|
it[SectorRangesTable.id],
|
||||||
|
it[SectorRangesTable.sectorId],
|
||||||
|
SelectionMode.valueOf(it[SectorRangesTable.type]),
|
||||||
|
it[SectorRangesTable.x1],
|
||||||
|
it[SectorRangesTable.y1],
|
||||||
|
it[SectorRangesTable.z1],
|
||||||
|
it[SectorRangesTable.x2],
|
||||||
|
it[SectorRangesTable.y2],
|
||||||
|
it[SectorRangesTable.z2],
|
||||||
|
it[SectorRangesTable.isSneaking]
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
package net.hareworks.hcu.landsector.service
|
||||||
|
|
||||||
|
import net.hareworks.hcu.landsector.model.SelectionData
|
||||||
|
import org.bukkit.Location
|
||||||
|
import java.util.UUID
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.PI
|
||||||
|
import kotlin.math.ceil
|
||||||
|
|
||||||
|
class SelectionService {
|
||||||
|
private val selections = ConcurrentHashMap<UUID, SelectionData>()
|
||||||
|
|
||||||
|
fun getSelection(uuid: UUID): SelectionData {
|
||||||
|
return selections.computeIfAbsent(uuid) { SelectionData() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearSelection(uuid: UUID) {
|
||||||
|
selections.remove(uuid)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAreaDetails(uuid: UUID): String {
|
||||||
|
val data = getSelection(uuid)
|
||||||
|
if (data.point1 == null || data.point2 == null) {
|
||||||
|
return "No complete selection."
|
||||||
|
}
|
||||||
|
|
||||||
|
val p1 = data.point1!!
|
||||||
|
val p2 = data.point2!!
|
||||||
|
|
||||||
|
return if (data.mode == net.hareworks.hcu.landsector.model.SelectionMode.CUBOID) {
|
||||||
|
val width: Int
|
||||||
|
val height: Int
|
||||||
|
val length: Int
|
||||||
|
|
||||||
|
if (!data.p1Sneaking) {
|
||||||
|
// Normal: P1 to P2
|
||||||
|
width = abs(p1.blockX - p2.blockX) + 1
|
||||||
|
height = abs(p1.blockY - p2.blockY) + 1
|
||||||
|
length = abs(p1.blockZ - p2.blockZ) + 1
|
||||||
|
} else {
|
||||||
|
// Sneak: P1 is center
|
||||||
|
val dx = abs(p1.blockX - p2.blockX)
|
||||||
|
val dy = abs(p1.blockY - p2.blockY)
|
||||||
|
val dz = abs(p1.blockZ - p2.blockZ)
|
||||||
|
width = dx * 2 + 1
|
||||||
|
height = dy * 2 + 1
|
||||||
|
length = dz * 2 + 1
|
||||||
|
}
|
||||||
|
"Cuboid: ${width}x${height}x${length} (${width * height * length} blocks)"
|
||||||
|
} else {
|
||||||
|
// Cylinder
|
||||||
|
val dx = p1.blockX - p2.blockX
|
||||||
|
val dz = p1.blockZ - p2.blockZ
|
||||||
|
// Use ceil(dist - 0.5) to find the minimum integer radius R such that (dist <= R + 0.5)
|
||||||
|
// This ensures the point is included in the R+0.5 boundary without overshooting to the next integer.
|
||||||
|
val dist = Math.sqrt((dx * dx + dz * dz).toDouble())
|
||||||
|
val baseRadius = ceil(dist - 0.5)
|
||||||
|
val radius = baseRadius + 0.5
|
||||||
|
|
||||||
|
val totalHeight = if (!data.p1Sneaking) {
|
||||||
|
// Normal: P1 center, symmetric height
|
||||||
|
val h = abs(p1.blockY - p2.blockY)
|
||||||
|
h * 2 + 1
|
||||||
|
} else {
|
||||||
|
// Sneak: P1 base, P2 top
|
||||||
|
abs(p1.blockY - p2.blockY) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
val volume = PI * radius * radius * totalHeight
|
||||||
|
"Cylinder: r=${"%.0f".format(baseRadius)}, h=$totalHeight (~${ceil(volume).toInt()} blocks)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCost(uuid: UUID): Double {
|
||||||
|
val data = getSelection(uuid)
|
||||||
|
if (data.point1 == null || data.point2 == null) {
|
||||||
|
return 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Placeholder cost logic: 1 block = 1 unit?
|
||||||
|
// Need to refine based on actual volume
|
||||||
|
val p1 = data.point1!!
|
||||||
|
val p2 = data.point2!!
|
||||||
|
|
||||||
|
val volume = if (data.mode == net.hareworks.hcu.landsector.model.SelectionMode.CUBOID) {
|
||||||
|
val width = abs(p1.blockX - p2.blockX) + 1
|
||||||
|
val height = abs(p1.blockY - p2.blockY) + 1
|
||||||
|
val length = abs(p1.blockZ - p2.blockZ) + 1
|
||||||
|
(width * height * length).toDouble()
|
||||||
|
} else {
|
||||||
|
val dx = p1.blockX - p2.blockX
|
||||||
|
val dz = p1.blockZ - p2.blockZ
|
||||||
|
val dist = Math.sqrt((dx * dx + dz * dz).toDouble())
|
||||||
|
val baseRadius = ceil(dist - 0.5)
|
||||||
|
val radius = baseRadius + 0.5
|
||||||
|
val height = abs(p1.blockY - p2.blockY) + 1
|
||||||
|
PI * radius * radius * height
|
||||||
|
}
|
||||||
|
|
||||||
|
return volume * 0.5 // Example rate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
package net.hareworks.hcu.landsector.task
|
||||||
|
|
||||||
|
import net.hareworks.hcu.landsector.LandSectorPlugin
|
||||||
|
import org.bukkit.Bukkit
|
||||||
|
import org.bukkit.Material
|
||||||
|
import org.bukkit.NamespacedKey
|
||||||
|
import org.bukkit.entity.BlockDisplay
|
||||||
|
import org.bukkit.entity.EntityType
|
||||||
|
import org.bukkit.persistence.PersistentDataType
|
||||||
|
import org.bukkit.scheduler.BukkitRunnable
|
||||||
|
import org.bukkit.util.Transformation
|
||||||
|
import org.joml.Quaternionf
|
||||||
|
import org.joml.Vector3f
|
||||||
|
import kotlin.math.PI
|
||||||
|
|
||||||
|
class SectorRotationTask(private val plugin: LandSectorPlugin) : BukkitRunnable() {
|
||||||
|
|
||||||
|
private val sectorKey = NamespacedKey(plugin, "sector_id")
|
||||||
|
private val scale = Vector3f(0.5f, 0.5f, 0.5f)
|
||||||
|
private val centerOffset = Vector3f(0.25f, 0.25f, 0.25f) // Scale 0.5 * Model Center (0.5, 0.5, 0.5)
|
||||||
|
|
||||||
|
override fun run() {
|
||||||
|
val sectors = plugin.sectorService?.getAllSectorsList() ?: return
|
||||||
|
// Bukkit.getLogger().info("Task running. Sectors count: ${sectors.size}")
|
||||||
|
|
||||||
|
sectors.forEach { sector ->
|
||||||
|
val world = Bukkit.getWorld(sector.world) ?: return@forEach
|
||||||
|
// Simple check if chunk is loaded to avoid loading chunks
|
||||||
|
val chunkX = sector.x shr 4
|
||||||
|
val chunkZ = sector.z shr 4
|
||||||
|
|
||||||
|
if (!world.isChunkLoaded(chunkX, chunkZ)) {
|
||||||
|
// Bukkit.getLogger().info("Chunk not loaded for sector ${sector.id}")
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check entities at the core location
|
||||||
|
val centerLoc = org.bukkit.Location(world, sector.x + 0.5, sector.y + 0.5, sector.z + 0.5)
|
||||||
|
|
||||||
|
// Search radius expanded to 2.0 to ensure hit
|
||||||
|
val entities = world.getNearbyEntities(centerLoc, 2.0, 2.0, 2.0)
|
||||||
|
|
||||||
|
// Bukkit.getLogger().info("Checking sector ${sector.id} at $centerLoc. Found entities: ${entities.size}")
|
||||||
|
|
||||||
|
entities.forEach { entity ->
|
||||||
|
if (entity.type == EntityType.BLOCK_DISPLAY && entity.persistentDataContainer.has(sectorKey, PersistentDataType.INTEGER)) {
|
||||||
|
val bd = entity as BlockDisplay
|
||||||
|
// Check if it's the command block
|
||||||
|
if (bd.block.material == Material.COMMAND_BLOCK) {
|
||||||
|
// Bukkit.getLogger().info("Rotater found target: ${bd.uniqueId}")
|
||||||
|
updateRotation(bd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateRotation(bd: BlockDisplay) {
|
||||||
|
// Continuous rotation based on time: 1 full rotation every 4 seconds
|
||||||
|
val periodMs = 4000L
|
||||||
|
val time = System.currentTimeMillis()
|
||||||
|
val phase = (time % periodMs).toDouble() / periodMs.toDouble()
|
||||||
|
val angle = (phase * 2 * Math.PI).toFloat() // 0 to 2PI
|
||||||
|
|
||||||
|
// Log for debug (once per 100 ticks approx to avoid spam? No, just once to verify)
|
||||||
|
// Bukkit.getLogger().info("Rotating BD: angle=$angle")
|
||||||
|
|
||||||
|
// Rotation around Y axis
|
||||||
|
val rot = Quaternionf().rotateY(angle)
|
||||||
|
|
||||||
|
val centerOffset = Vector3f(0.25f, 0.25f, 0.25f) // The center in Scaled space
|
||||||
|
// T = - (R * C)
|
||||||
|
val rotatedOffset = Vector3f(centerOffset).rotate(rot)
|
||||||
|
val newTranslation = Vector3f(rotatedOffset).negate()
|
||||||
|
|
||||||
|
val newTrans = Transformation(
|
||||||
|
newTranslation,
|
||||||
|
rot,
|
||||||
|
scale,
|
||||||
|
Quaternionf(0f, 0f, 0f, 1f)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Interpolation settings
|
||||||
|
bd.interpolationDuration = 3 // Short duration to match high frequency update
|
||||||
|
bd.interpolationDelay = -1 // Start immediately
|
||||||
|
bd.transformation = newTrans
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,195 @@
|
||||||
|
package net.hareworks.hcu.landsector.task
|
||||||
|
|
||||||
|
import net.hareworks.hcu.landsector.model.SelectionMode
|
||||||
|
import net.hareworks.hcu.landsector.service.SelectionService
|
||||||
|
import net.hareworks.hcu.visualizer.GeometryVisualizer
|
||||||
|
import net.kyori.adventure.text.Component
|
||||||
|
import net.kyori.adventure.text.format.NamedTextColor
|
||||||
|
import org.bukkit.Bukkit
|
||||||
|
import org.bukkit.Color
|
||||||
|
import org.bukkit.Material
|
||||||
|
import org.bukkit.NamespacedKey
|
||||||
|
import org.bukkit.entity.Player
|
||||||
|
import org.bukkit.inventory.EquipmentSlot
|
||||||
|
import org.bukkit.persistence.PersistentDataType
|
||||||
|
import org.bukkit.plugin.java.JavaPlugin
|
||||||
|
import org.bukkit.scheduler.BukkitRunnable
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
class SelectionVisualizerTask(
|
||||||
|
private val plugin: JavaPlugin,
|
||||||
|
private val selectionService: SelectionService
|
||||||
|
) : BukkitRunnable() {
|
||||||
|
|
||||||
|
override fun run() {
|
||||||
|
for (player in Bukkit.getOnlinePlayers()) {
|
||||||
|
visualize(player)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun visualize(player: Player) {
|
||||||
|
// Check if holding tool
|
||||||
|
// Check if tool is in inventory
|
||||||
|
val key = NamespacedKey(plugin, "component")
|
||||||
|
val hasTool = player.inventory.contents.any { item ->
|
||||||
|
item != null && item.type == Material.FLINT &&
|
||||||
|
item.itemMeta?.persistentDataContainer?.get(key, PersistentDataType.STRING) == "land_sector_tool"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasTool) return
|
||||||
|
|
||||||
|
val selection = selectionService.getSelection(player.uniqueId)
|
||||||
|
val p1 = selection.point1 ?: return
|
||||||
|
|
||||||
|
// Determine P2: Either set in selection, or dynamic based on cursor if not set (or we show review for both?)
|
||||||
|
// If p2 is set, visualize that.
|
||||||
|
// If p2 is NOT set, visualize dynamic preview using target block.
|
||||||
|
|
||||||
|
var p2 = selection.point2
|
||||||
|
var isDynamic = false
|
||||||
|
|
||||||
|
if (p2 == null) {
|
||||||
|
val target = player.getTargetBlockExact(30)
|
||||||
|
if (target != null) {
|
||||||
|
p2 = target.location
|
||||||
|
isDynamic = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p2 == null) return // No p2 and no target
|
||||||
|
|
||||||
|
// If different world, ignore
|
||||||
|
if (p1.world != p2.world) return
|
||||||
|
|
||||||
|
if (selection.mode == SelectionMode.CUBOID) {
|
||||||
|
// Cuboid visualization
|
||||||
|
var minX: Double
|
||||||
|
var minY: Double
|
||||||
|
var minZ: Double
|
||||||
|
var maxX: Double
|
||||||
|
var maxY: Double
|
||||||
|
var maxZ: Double
|
||||||
|
|
||||||
|
if (!selection.p1Sneaking) {
|
||||||
|
// Normal: corner to corner
|
||||||
|
minX = minOf(p1.blockX, p2.blockX).toDouble()
|
||||||
|
minY = minOf(p1.blockY, p2.blockY).toDouble()
|
||||||
|
minZ = minOf(p1.blockZ, p2.blockZ).toDouble()
|
||||||
|
maxX = maxOf(p1.blockX, p2.blockX).toDouble() + 1.0
|
||||||
|
maxY = maxOf(p1.blockY, p2.blockY).toDouble() + 1.0
|
||||||
|
maxZ = maxOf(p1.blockZ, p2.blockZ).toDouble() + 1.0
|
||||||
|
} else {
|
||||||
|
// Sneak: P1 center, P2 defines radius
|
||||||
|
val dx = abs(p1.blockX - p2.blockX)
|
||||||
|
val dy = abs(p1.blockY - p2.blockY)
|
||||||
|
val dz = abs(p1.blockZ - p2.blockZ)
|
||||||
|
|
||||||
|
minX = (p1.blockX - dx).toDouble()
|
||||||
|
maxX = (p1.blockX + dx).toDouble() + 1.0
|
||||||
|
minY = (p1.blockY - dy).toDouble()
|
||||||
|
maxY = (p1.blockY + dy).toDouble() + 1.0
|
||||||
|
minZ = (p1.blockZ - dz).toDouble()
|
||||||
|
maxZ = (p1.blockZ + dz).toDouble() + 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
GeometryVisualizer.drawCuboid(player, minX, minY, minZ, maxX, maxY, maxZ)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Cylinder visualization
|
||||||
|
val centerX: Double
|
||||||
|
val centerY: Double
|
||||||
|
val centerZ: Double
|
||||||
|
val radius: Double
|
||||||
|
val minY: Double
|
||||||
|
val maxY: Double
|
||||||
|
|
||||||
|
val dx = p1.blockX - p2.blockX
|
||||||
|
val dz = p1.blockZ - p2.blockZ
|
||||||
|
val dist = Math.sqrt((dx * dx + dz * dz).toDouble())
|
||||||
|
radius = kotlin.math.ceil(dist - 0.5)
|
||||||
|
|
||||||
|
if (!selection.p1Sneaking) {
|
||||||
|
// Normal: P1 center, symmetric height based on P2.y diff
|
||||||
|
centerX = p1.blockX + 0.5
|
||||||
|
centerY = p1.blockY + 0.5 // Logic center
|
||||||
|
centerZ = p1.blockZ + 0.5
|
||||||
|
|
||||||
|
val hDiff = abs(p1.blockY - p2.blockY)
|
||||||
|
// Bottom and Top
|
||||||
|
// From center block center, go down hDiff blocks (and include center block?)
|
||||||
|
// If P1.y = 10, P2.y = 12. Diff 2.
|
||||||
|
// Blocks: 8, 9, 10, 11, 12. Range [8, 12]. Height 5. (2*2 + 1)
|
||||||
|
|
||||||
|
// Block coords:
|
||||||
|
val baseBlockY = p1.blockY - hDiff
|
||||||
|
val topBlockY = p1.blockY + hDiff
|
||||||
|
|
||||||
|
minY = baseBlockY.toDouble()
|
||||||
|
maxY = topBlockY.toDouble() + 1.0
|
||||||
|
} else {
|
||||||
|
// Sneak: P1 base center. P2 top center.
|
||||||
|
centerX = p1.blockX + 0.5
|
||||||
|
centerZ = p1.blockZ + 0.5
|
||||||
|
// Only height from p2
|
||||||
|
val y1 = p1.blockY
|
||||||
|
val y2 = p2.blockY
|
||||||
|
|
||||||
|
val baseBlockY = minOf(y1, y2)
|
||||||
|
val topBlockY = maxOf(y1, y2)
|
||||||
|
|
||||||
|
minY = baseBlockY.toDouble()
|
||||||
|
maxY = topBlockY.toDouble() + 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate blocks for cylinder outline
|
||||||
|
val cX = kotlin.math.floor(centerX).toInt()
|
||||||
|
val cZ = kotlin.math.floor(centerZ).toInt()
|
||||||
|
val blocks = mutableSetOf<Pair<Int, Int>>()
|
||||||
|
// Add 0.5 to radius to encompass the full block width of the boundary blocks
|
||||||
|
val actualRadius = radius + 0.5
|
||||||
|
val radiusSq = actualRadius * actualRadius
|
||||||
|
val rInt = actualRadius.toInt() + 1
|
||||||
|
|
||||||
|
for (dx in -rInt..rInt) {
|
||||||
|
for (dz in -rInt..rInt) {
|
||||||
|
// Check if block center is within the radius
|
||||||
|
if (dx * dx + dz * dz <= radiusSq) {
|
||||||
|
blocks.add(Pair(cX + dx, cZ + dz))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Draw wireframe circle (smooth) layered with block outline
|
||||||
|
// Wireframe uses the expanded radius to match the block outline visuals
|
||||||
|
GeometryVisualizer.drawCylinder(player, centerX, p1.blockY.toDouble(), centerZ, radius.toInt(), minY, maxY)
|
||||||
|
|
||||||
|
val color = Color.fromRGB(100, 200, 255)
|
||||||
|
// Draw bottom surface outline
|
||||||
|
GeometryVisualizer.drawBlockSurfaceOutline(
|
||||||
|
player,
|
||||||
|
minY,
|
||||||
|
blocks,
|
||||||
|
{ _, _, _ -> false },
|
||||||
|
color,
|
||||||
|
minY
|
||||||
|
)
|
||||||
|
|
||||||
|
// Draw top surface outline
|
||||||
|
GeometryVisualizer.drawBlockSurfaceOutline(
|
||||||
|
player,
|
||||||
|
maxY,
|
||||||
|
blocks,
|
||||||
|
{ _, _, _ -> false },
|
||||||
|
color,
|
||||||
|
maxY
|
||||||
|
)
|
||||||
|
|
||||||
|
// Vertical edges for the outline?
|
||||||
|
// The current generic visualizer doesn't have a specific "drawVerticalConnectors" for this map-based approach easily accessible
|
||||||
|
// or we'd have to iterate edges.
|
||||||
|
// For now, surface outlines should be sufficient as requested.
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -32,6 +32,9 @@ dependencies {
|
||||||
compileOnly("net.hareworks:permits-lib")
|
compileOnly("net.hareworks:permits-lib")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0")
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0")
|
||||||
|
|
||||||
|
// Visualizer Lib (Bundled & Relocated)
|
||||||
|
implementation("net.hareworks.hcu:visualizer-lib:1.0")
|
||||||
|
|
||||||
// Database (Exposed v1)
|
// Database (Exposed v1)
|
||||||
compileOnly("org.jetbrains.exposed:exposed-core:$exposedVersion")
|
compileOnly("org.jetbrains.exposed:exposed-core:$exposedVersion")
|
||||||
compileOnly("org.jetbrains.exposed:exposed-dao:$exposedVersion")
|
compileOnly("org.jetbrains.exposed:exposed-dao:$exposedVersion")
|
||||||
|
|
@ -55,6 +58,8 @@ tasks {
|
||||||
exclude(dependency("org.jetbrains.kotlin:kotlin-stdlib-jdk8"))
|
exclude(dependency("org.jetbrains.kotlin:kotlin-stdlib-jdk8"))
|
||||||
exclude(dependency("org.jetbrains.kotlin:kotlin-stdlib-jdk7"))
|
exclude(dependency("org.jetbrains.kotlin:kotlin-stdlib-jdk7"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
relocate("net.hareworks.hcu.visualizer", "net.hareworks.hcu.landsector.libs.visualizer")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import net.hareworks.hcu.core.Main
|
||||||
import net.hareworks.hcu.core.player.PlayerIdService
|
import net.hareworks.hcu.core.player.PlayerIdService
|
||||||
import net.hareworks.hcu.landsector.command.LandSectorCommand
|
import net.hareworks.hcu.landsector.command.LandSectorCommand
|
||||||
import net.hareworks.hcu.landsector.listener.SectorListener
|
import net.hareworks.hcu.landsector.listener.SectorListener
|
||||||
|
import net.hareworks.hcu.landsector.service.SelectionService
|
||||||
import net.hareworks.hcu.landsector.service.SectorService
|
import net.hareworks.hcu.landsector.service.SectorService
|
||||||
import org.bukkit.plugin.java.JavaPlugin
|
import org.bukkit.plugin.java.JavaPlugin
|
||||||
|
|
||||||
|
|
@ -15,10 +16,13 @@ class LandSectorPlugin : JavaPlugin() {
|
||||||
}
|
}
|
||||||
|
|
||||||
var sectorService: SectorService? = null
|
var sectorService: SectorService? = null
|
||||||
|
var selectionService: SelectionService? = null
|
||||||
|
|
||||||
override fun onEnable() {
|
override fun onEnable() {
|
||||||
instance = this
|
instance = this
|
||||||
|
|
||||||
|
selectionService = SelectionService()
|
||||||
|
|
||||||
// Register commands
|
// Register commands
|
||||||
LandSectorCommand(this).register()
|
LandSectorCommand(this).register()
|
||||||
|
|
||||||
|
|
@ -57,7 +61,16 @@ class LandSectorPlugin : JavaPlugin() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
server.pluginManager.registerEvents(SectorListener(this, service, pIdService), this)
|
val selService = selectionService
|
||||||
|
if (selService != null) {
|
||||||
|
server.pluginManager.registerEvents(SectorListener(this, service, pIdService, selService), this)
|
||||||
|
server.pluginManager.registerEvents(net.hareworks.hcu.landsector.listener.SelectionListener(this, selService, service), this)
|
||||||
|
|
||||||
|
// Schedule visualization task
|
||||||
|
net.hareworks.hcu.landsector.task.SelectionVisualizerTask(this, selService).runTaskTimer(this, 0L, 4L)
|
||||||
|
} else {
|
||||||
|
logger.severe("SelectionService not initialized!")
|
||||||
|
}
|
||||||
|
|
||||||
// Schedule auto-save every 5 minutes
|
// Schedule auto-save every 5 minutes
|
||||||
server.scheduler.runTaskTimerAsynchronously(this, Runnable {
|
server.scheduler.runTaskTimerAsynchronously(this, Runnable {
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,45 @@ class LandSectorCommand(private val landSectorPlugin: LandSectorPlugin) {
|
||||||
fun register() {
|
fun register() {
|
||||||
kommand(landSectorPlugin) {
|
kommand(landSectorPlugin) {
|
||||||
command("landsector") {
|
command("landsector") {
|
||||||
|
literal("givetool") {
|
||||||
|
executes {
|
||||||
|
val player = sender as? Player ?: return@executes
|
||||||
|
giveTool(player, null)
|
||||||
|
}
|
||||||
|
integer("sectorId") {
|
||||||
|
executes {
|
||||||
|
val player = sender as? Player ?: return@executes
|
||||||
|
val id = argument<Int>("sectorId")
|
||||||
|
giveTool(player, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
literal("activate") {
|
||||||
|
integer("sectorId") {
|
||||||
|
executes {
|
||||||
|
val id = argument<Int>("sectorId")
|
||||||
|
sender.sendMessage(Component.text("Activation for Sector #$id pending implementation.", NamedTextColor.YELLOW))
|
||||||
|
// TODO: Validate selection and lock in
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
literal("cancel") {
|
||||||
|
integer("sectorId") {
|
||||||
|
executes {
|
||||||
|
val id = argument<Int>("sectorId")
|
||||||
|
sender.sendMessage(Component.text("Cancellation for Sector #$id pending implementation.", NamedTextColor.YELLOW))
|
||||||
|
// TODO: Cancel logic
|
||||||
|
val player = sender as? Player
|
||||||
|
if (player != null) {
|
||||||
|
landSectorPlugin.selectionService?.clearSelection(player.uniqueId)
|
||||||
|
sender.sendMessage(Component.text("Selection cleared.", NamedTextColor.RED))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
literal("give") {
|
literal("give") {
|
||||||
executes {
|
executes {
|
||||||
val player = sender as? Player ?: return@executes
|
val player = sender as? Player ?: return@executes
|
||||||
|
|
@ -129,4 +168,31 @@ class LandSectorCommand(private val landSectorPlugin: LandSectorPlugin) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun giveTool(player: Player, sectorId: Int?) {
|
||||||
|
val item = ItemStack(Material.FLINT)
|
||||||
|
val meta = item.itemMeta
|
||||||
|
meta.displayName(Component.text("Range Selection Tool" + (if (sectorId != null) " (#$sectorId)" else ""), NamedTextColor.AQUA))
|
||||||
|
meta.persistentDataContainer.set(
|
||||||
|
NamespacedKey(landSectorPlugin, "component"),
|
||||||
|
PersistentDataType.STRING,
|
||||||
|
"land_sector_tool"
|
||||||
|
)
|
||||||
|
if (sectorId != null) {
|
||||||
|
meta.persistentDataContainer.set(
|
||||||
|
NamespacedKey(landSectorPlugin, "sector_id"),
|
||||||
|
PersistentDataType.INTEGER,
|
||||||
|
sectorId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
meta.lore(listOf(
|
||||||
|
Component.text("Left Click: Switch Mode", NamedTextColor.GRAY),
|
||||||
|
Component.text("Right Click: Select Position", NamedTextColor.GRAY),
|
||||||
|
Component.text("Sneaking acts as modifier", NamedTextColor.DARK_GRAY)
|
||||||
|
) + if (sectorId != null) listOf(Component.text("Linked to Sector #$sectorId", NamedTextColor.GOLD)) else emptyList())
|
||||||
|
item.itemMeta = meta
|
||||||
|
|
||||||
|
player.inventory.addItem(item)
|
||||||
|
player.sendMessage(Component.text("Gave Range Selection Tool${if (sectorId != null) " for Sector #$sectorId" else ""}.", NamedTextColor.GREEN))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,3 +13,18 @@ object SectorsTable : Table("land_sectors") {
|
||||||
|
|
||||||
override val primaryKey = PrimaryKey(id)
|
override val primaryKey = PrimaryKey(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
object SectorRangesTable : Table("land_sector_ranges") {
|
||||||
|
val id = integer("id").autoIncrement()
|
||||||
|
val sectorId = integer("sector_id").references(SectorsTable.id)
|
||||||
|
val type = varchar("type", 16)
|
||||||
|
val x1 = integer("x1")
|
||||||
|
val y1 = integer("y1")
|
||||||
|
val z1 = integer("z1")
|
||||||
|
val x2 = integer("x2")
|
||||||
|
val y2 = integer("y2")
|
||||||
|
val z2 = integer("z2")
|
||||||
|
val isSneaking = bool("is_sneaking")
|
||||||
|
|
||||||
|
override val primaryKey = PrimaryKey(id)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,10 +27,15 @@ import org.joml.Vector3f
|
||||||
import org.joml.Quaternionf
|
import org.joml.Quaternionf
|
||||||
import org.bukkit.block.data.type.Slab
|
import org.bukkit.block.data.type.Slab
|
||||||
|
|
||||||
|
import net.hareworks.hcu.landsector.service.SelectionService
|
||||||
|
import net.kyori.adventure.text.event.ClickEvent
|
||||||
|
import org.bukkit.event.player.PlayerInteractEntityEvent
|
||||||
|
|
||||||
class SectorListener(
|
class SectorListener(
|
||||||
private val plugin: LandSectorPlugin,
|
private val plugin: LandSectorPlugin,
|
||||||
private val sectorService: SectorService,
|
private val sectorService: SectorService,
|
||||||
private val playerIdService: PlayerIdService
|
private val playerIdService: PlayerIdService,
|
||||||
|
private val selectionService: SelectionService
|
||||||
) : Listener {
|
) : Listener {
|
||||||
|
|
||||||
@EventHandler
|
@EventHandler
|
||||||
|
|
@ -194,6 +199,78 @@ class SectorListener(
|
||||||
player.sendMessage(Component.text("Sector Core placed!", NamedTextColor.GREEN))
|
player.sendMessage(Component.text("Sector Core placed!", NamedTextColor.GREEN))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
fun onInteractEntity(event: PlayerInteractEntityEvent) {
|
||||||
|
if (event.hand != org.bukkit.inventory.EquipmentSlot.HAND) return
|
||||||
|
val entity = event.rightClicked
|
||||||
|
if (entity !is Shulker) return
|
||||||
|
|
||||||
|
val sectorKey = NamespacedKey(plugin, "sector_id")
|
||||||
|
if (!entity.persistentDataContainer.has(sectorKey, PersistentDataType.INTEGER)) return
|
||||||
|
|
||||||
|
val sectorId = entity.persistentDataContainer.get(sectorKey, PersistentDataType.INTEGER) ?: return
|
||||||
|
val player = event.player
|
||||||
|
|
||||||
|
// Ownership Check
|
||||||
|
val pEntry = playerIdService.find(player.uniqueId)
|
||||||
|
if (pEntry == null) return
|
||||||
|
val sector = sectorService.getSector(sectorId) ?: return
|
||||||
|
|
||||||
|
if (sector.ownerActorId != pEntry.actorId) {
|
||||||
|
player.sendMessage(Component.text("You are not the owner of this sector.", NamedTextColor.RED))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open GUI
|
||||||
|
val gui = org.bukkit.Bukkit.createInventory(null, 27, Component.text("Sector Manager (#${sectorId})", NamedTextColor.BLACK))
|
||||||
|
|
||||||
|
// Info Item with Ranges
|
||||||
|
val ranges = sectorService.getRanges(sectorId)
|
||||||
|
val rangeLore = ranges.map { Component.text(" - [${it.id}] ${it.type}", NamedTextColor.GRAY) }
|
||||||
|
val infoLore = mutableListOf<Component>()
|
||||||
|
infoLore.add(Component.text("Sector ID: $sectorId", NamedTextColor.WHITE))
|
||||||
|
infoLore.addAll(rangeLore)
|
||||||
|
|
||||||
|
gui.setItem(4, createGuiItem(
|
||||||
|
Material.PAPER,
|
||||||
|
Component.text("Sector Info", NamedTextColor.GOLD),
|
||||||
|
infoLore,
|
||||||
|
sectorId,
|
||||||
|
"info"
|
||||||
|
))
|
||||||
|
|
||||||
|
// Get Selection Tool
|
||||||
|
gui.setItem(11, createGuiItem(
|
||||||
|
Material.FLINT,
|
||||||
|
Component.text("Get Selection Tool", NamedTextColor.AQUA),
|
||||||
|
listOf(Component.text("Click to receive the selection tool.", NamedTextColor.GRAY)),
|
||||||
|
sectorId,
|
||||||
|
"give_tool"
|
||||||
|
))
|
||||||
|
|
||||||
|
// Activate
|
||||||
|
gui.setItem(13, createGuiItem(
|
||||||
|
Material.LIME_CONCRETE,
|
||||||
|
Component.text("Activate Sector", NamedTextColor.GREEN),
|
||||||
|
listOf(Component.text("Click to activate this sector.", NamedTextColor.GRAY)),
|
||||||
|
sectorId,
|
||||||
|
"activate"
|
||||||
|
))
|
||||||
|
|
||||||
|
// Cancel / Destroy
|
||||||
|
gui.setItem(15, createGuiItem(
|
||||||
|
Material.RED_CONCRETE,
|
||||||
|
Component.text("Cancel / Destroy", NamedTextColor.RED),
|
||||||
|
listOf(Component.text("Click to destroy this sector.", NamedTextColor.GRAY)),
|
||||||
|
sectorId,
|
||||||
|
"cancel"
|
||||||
|
))
|
||||||
|
|
||||||
|
player.openInventory(gui)
|
||||||
|
|
||||||
|
event.isCancelled = true
|
||||||
|
}
|
||||||
|
|
||||||
@EventHandler
|
@EventHandler
|
||||||
fun onDamage(event: EntityDamageEvent) {
|
fun onDamage(event: EntityDamageEvent) {
|
||||||
val entity = event.entity
|
val entity = event.entity
|
||||||
|
|
@ -343,4 +420,47 @@ class SectorListener(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
fun onInventoryClick(event: org.bukkit.event.inventory.InventoryClickEvent) {
|
||||||
|
val item = event.currentItem ?: return
|
||||||
|
val meta = item.itemMeta ?: return
|
||||||
|
val pluginKey = NamespacedKey(plugin, "gui_action")
|
||||||
|
|
||||||
|
if (!meta.persistentDataContainer.has(pluginKey, PersistentDataType.STRING)) return
|
||||||
|
|
||||||
|
event.isCancelled = true // Prevent taking items
|
||||||
|
|
||||||
|
val player = event.whoClicked as? Player ?: return
|
||||||
|
val action = meta.persistentDataContainer.get(pluginKey, PersistentDataType.STRING)
|
||||||
|
val sectorKey = NamespacedKey(plugin, "sector_id")
|
||||||
|
val sectorId = meta.persistentDataContainer.get(sectorKey, PersistentDataType.INTEGER) ?: return
|
||||||
|
|
||||||
|
player.closeInventory()
|
||||||
|
|
||||||
|
when (action) {
|
||||||
|
"give_tool" -> player.performCommand("landsector givetool $sectorId")
|
||||||
|
"activate" -> player.performCommand("landsector activate $sectorId")
|
||||||
|
"cancel" -> player.performCommand("landsector cancel $sectorId")
|
||||||
|
"info" -> {
|
||||||
|
player.playSound(player.location, Sound.UI_BUTTON_CLICK, 1f, 1f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createGuiItem(material: Material, name: Component, lore: List<Component>, sectorId: Int, action: String): org.bukkit.inventory.ItemStack {
|
||||||
|
val item = org.bukkit.inventory.ItemStack(material)
|
||||||
|
val meta = item.itemMeta
|
||||||
|
meta.displayName(name)
|
||||||
|
meta.lore(lore)
|
||||||
|
|
||||||
|
val actionKey = NamespacedKey(plugin, "gui_action")
|
||||||
|
val sectorKey = NamespacedKey(plugin, "sector_id")
|
||||||
|
|
||||||
|
meta.persistentDataContainer.set(actionKey, PersistentDataType.STRING, action)
|
||||||
|
meta.persistentDataContainer.set(sectorKey, PersistentDataType.INTEGER, sectorId)
|
||||||
|
|
||||||
|
item.itemMeta = meta
|
||||||
|
return item
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,188 @@
|
||||||
|
package net.hareworks.hcu.landsector.listener
|
||||||
|
|
||||||
|
import net.hareworks.hcu.landsector.LandSectorPlugin
|
||||||
|
import net.hareworks.hcu.landsector.service.SelectionService
|
||||||
|
import net.hareworks.hcu.landsector.service.SectorService
|
||||||
|
import net.hareworks.hcu.landsector.model.SelectionMode
|
||||||
|
import net.kyori.adventure.text.Component
|
||||||
|
import net.kyori.adventure.text.format.NamedTextColor
|
||||||
|
import org.bukkit.Material
|
||||||
|
import org.bukkit.NamespacedKey
|
||||||
|
import org.bukkit.event.EventHandler
|
||||||
|
import org.bukkit.event.Listener
|
||||||
|
import org.bukkit.event.block.Action
|
||||||
|
import org.bukkit.event.player.PlayerInteractEvent
|
||||||
|
import org.bukkit.persistence.PersistentDataType
|
||||||
|
import org.bukkit.inventory.EquipmentSlot
|
||||||
|
|
||||||
|
class SelectionListener(
|
||||||
|
private val plugin: LandSectorPlugin,
|
||||||
|
private val selectionService: SelectionService,
|
||||||
|
private val sectorService: SectorService
|
||||||
|
) : Listener {
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
fun onInteract(event: PlayerInteractEvent) {
|
||||||
|
val item = event.item ?: return
|
||||||
|
if (item.type != Material.FLINT) return
|
||||||
|
|
||||||
|
val key = NamespacedKey(plugin, "component")
|
||||||
|
if (item.itemMeta?.persistentDataContainer?.get(key, PersistentDataType.STRING) != "land_sector_tool") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only Main Hand to avoid double fire
|
||||||
|
if (event.hand != EquipmentSlot.HAND) return
|
||||||
|
|
||||||
|
val player = event.player
|
||||||
|
val selection = selectionService.getSelection(player.uniqueId)
|
||||||
|
|
||||||
|
// Left Click: Switch Mode
|
||||||
|
if (event.action == Action.LEFT_CLICK_AIR || event.action == Action.LEFT_CLICK_BLOCK) {
|
||||||
|
event.isCancelled = true // Prevent breaking block
|
||||||
|
|
||||||
|
selection.mode = if (selection.mode == SelectionMode.CUBOID) SelectionMode.CYLINDER else SelectionMode.CUBOID
|
||||||
|
player.sendMessage(Component.text("Mode switched to: ${selection.mode}", NamedTextColor.YELLOW))
|
||||||
|
|
||||||
|
// Reset selection on mode switch
|
||||||
|
selection.point1 = null
|
||||||
|
selection.point2 = null
|
||||||
|
selection.p1Sneaking = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Right Click: Set Points
|
||||||
|
if (event.action == Action.RIGHT_CLICK_BLOCK) {
|
||||||
|
event.isCancelled = true // Prevent placing
|
||||||
|
|
||||||
|
val clickedBlock = event.clickedBlock ?: return
|
||||||
|
val loc = clickedBlock.location
|
||||||
|
|
||||||
|
if (selection.point1 == null) {
|
||||||
|
// Set Point 1
|
||||||
|
selection.point1 = loc
|
||||||
|
selection.p1Sneaking = player.isSneaking
|
||||||
|
selection.point2 = null // Clear p2 just in case
|
||||||
|
|
||||||
|
player.sendMessage(Component.text("Position 1 set at ${loc.blockX}, ${loc.blockY}, ${loc.blockZ} (Mode: ${selection.mode}, Sneaking: ${selection.p1Sneaking})", NamedTextColor.GREEN))
|
||||||
|
} else {
|
||||||
|
// Set Point 2
|
||||||
|
// If P2 is already set, reset to P1 new? standard standard is cyclic.
|
||||||
|
if (selection.point2 != null) {
|
||||||
|
// Resetting, treat as P1
|
||||||
|
selection.point1 = loc
|
||||||
|
selection.p1Sneaking = player.isSneaking
|
||||||
|
selection.point2 = null
|
||||||
|
player.sendMessage(Component.text("Selection reset. Position 1 set at ${loc.blockX}, ${loc.blockY}, ${loc.blockZ} (Mode: ${selection.mode}, Sneaking: ${selection.p1Sneaking})", NamedTextColor.GREEN))
|
||||||
|
} else {
|
||||||
|
selection.point2 = loc
|
||||||
|
// Check for sector ID
|
||||||
|
val itemStack = event.item
|
||||||
|
val sectorKey = NamespacedKey(plugin, "sector_id")
|
||||||
|
if (itemStack != null && itemStack.itemMeta.persistentDataContainer.has(sectorKey, PersistentDataType.INTEGER)) {
|
||||||
|
val sId = itemStack.itemMeta.persistentDataContainer.get(sectorKey, PersistentDataType.INTEGER)!!
|
||||||
|
val p1 = selection.point1!!
|
||||||
|
val p2 = selection.point2!!
|
||||||
|
|
||||||
|
// Add Range
|
||||||
|
val range = sectorService.addRange(
|
||||||
|
sId,
|
||||||
|
selection.mode,
|
||||||
|
p1.blockX, p1.blockY, p1.blockZ,
|
||||||
|
p2.blockX, p2.blockY, p2.blockZ,
|
||||||
|
selection.p1Sneaking
|
||||||
|
)
|
||||||
|
|
||||||
|
player.sendMessage(Component.text("Range added to Sector #$sId.", NamedTextColor.GREEN))
|
||||||
|
|
||||||
|
// Clear selection
|
||||||
|
selection.point1 = null
|
||||||
|
selection.point2 = null
|
||||||
|
} else {
|
||||||
|
player.sendMessage(Component.text("Position 2 set at ${loc.blockX}, ${loc.blockY}, ${loc.blockZ}", NamedTextColor.GREEN))
|
||||||
|
|
||||||
|
// Show Cost / Area
|
||||||
|
player.sendMessage(Component.text(selectionService.getAreaDetails(player.uniqueId), NamedTextColor.AQUA))
|
||||||
|
player.sendMessage(Component.text("Cost: ${selectionService.getCost(player.uniqueId)}", NamedTextColor.GOLD))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
fun onDrop(event: org.bukkit.event.player.PlayerDropItemEvent) {
|
||||||
|
val item = event.itemDrop.itemStack
|
||||||
|
val key = NamespacedKey(plugin, "component")
|
||||||
|
if (item.itemMeta?.persistentDataContainer?.get(key, PersistentDataType.STRING) == "land_sector_tool") {
|
||||||
|
event.isCancelled = true
|
||||||
|
event.player.sendMessage(Component.text("You cannot drop this tool.", NamedTextColor.RED))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
fun onInventoryClick(event: org.bukkit.event.inventory.InventoryClickEvent) {
|
||||||
|
val player = event.whoClicked as? org.bukkit.entity.Player ?: return
|
||||||
|
val clickedInv = event.clickedInventory
|
||||||
|
val action = event.action
|
||||||
|
|
||||||
|
fun isTool(stack: org.bukkit.inventory.ItemStack?): Boolean {
|
||||||
|
if (stack == null || stack.type != Material.FLINT) return false
|
||||||
|
val key = NamespacedKey(plugin, "component")
|
||||||
|
return stack.itemMeta?.persistentDataContainer?.get(key, PersistentDataType.STRING) == "land_sector_tool"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Prevent dropping via inventory
|
||||||
|
if (action.name.startsWith("DROP")) {
|
||||||
|
if (isTool(event.currentItem) || isTool(event.cursor)) {
|
||||||
|
event.isCancelled = true
|
||||||
|
player.sendMessage(Component.text("You cannot drop this tool.", NamedTextColor.RED))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Prevent interaction with tool in external inventories (Top Inventory)
|
||||||
|
// If clicking inside an inventory that is NOT the player's inventory
|
||||||
|
if (clickedInv != null && clickedInv != player.inventory) {
|
||||||
|
// If trying to place tool (Cursor has tool)
|
||||||
|
if (isTool(event.cursor)) {
|
||||||
|
event.isCancelled = true
|
||||||
|
player.sendMessage(Component.text("You cannot store this tool.", NamedTextColor.RED))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If trying to swap tool from hotbar (Number key)
|
||||||
|
if (event.click == org.bukkit.event.inventory.ClickType.NUMBER_KEY) {
|
||||||
|
val swappedItem = player.inventory.getItem(event.hotbarButton)
|
||||||
|
if (isTool(swappedItem)) {
|
||||||
|
event.isCancelled = true
|
||||||
|
player.sendMessage(Component.text("You cannot store this tool.", NamedTextColor.RED))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If trying to take tool IS in external inventory? (Shouldn't happen, but prevent taking just in case)
|
||||||
|
// This safeguards if somehow it got there.
|
||||||
|
if (isTool(event.currentItem)) {
|
||||||
|
event.isCancelled = true
|
||||||
|
// Allow them to break the item? No, just block interaction.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Prevent Shift-Clicking tool FROM player inventory TO external inventory
|
||||||
|
if (clickedInv == player.inventory && event.isShiftClick) {
|
||||||
|
if (isTool(event.currentItem)) {
|
||||||
|
// If top inventory is NOT Crafting/Creative (Personal), block transfer
|
||||||
|
val topType = event.view.topInventory.type
|
||||||
|
if (topType != org.bukkit.event.inventory.InventoryType.CRAFTING &&
|
||||||
|
topType != org.bukkit.event.inventory.InventoryType.CREATIVE) {
|
||||||
|
|
||||||
|
event.isCancelled = true
|
||||||
|
player.sendMessage(Component.text("You cannot store this tool.", NamedTextColor.RED))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
package net.hareworks.hcu.landsector.model
|
||||||
|
|
||||||
|
data class SectorRange(
|
||||||
|
val id: Int,
|
||||||
|
val sectorId: Int,
|
||||||
|
val type: SelectionMode,
|
||||||
|
val x1: Int,
|
||||||
|
val y1: Int,
|
||||||
|
val z1: Int,
|
||||||
|
val x2: Int,
|
||||||
|
val y2: Int,
|
||||||
|
val z2: Int,
|
||||||
|
val isSneaking: Boolean
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
package net.hareworks.hcu.landsector.model
|
||||||
|
|
||||||
|
import org.bukkit.Location
|
||||||
|
|
||||||
|
enum class SelectionMode {
|
||||||
|
CUBOID,
|
||||||
|
CYLINDER
|
||||||
|
}
|
||||||
|
|
||||||
|
data class SelectionData(
|
||||||
|
var mode: SelectionMode = SelectionMode.CUBOID,
|
||||||
|
var point1: Location? = null,
|
||||||
|
var point2: Location? = null,
|
||||||
|
var p1Sneaking: Boolean = false
|
||||||
|
)
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
package net.hareworks.hcu.landsector.service
|
package net.hareworks.hcu.landsector.service
|
||||||
|
|
||||||
import net.hareworks.hcu.landsector.database.SectorsTable
|
import net.hareworks.hcu.landsector.database.SectorsTable
|
||||||
|
import net.hareworks.hcu.landsector.database.SectorRangesTable
|
||||||
import net.hareworks.hcu.landsector.model.Sector
|
import net.hareworks.hcu.landsector.model.Sector
|
||||||
|
import net.hareworks.hcu.landsector.model.SectorRange
|
||||||
|
import net.hareworks.hcu.landsector.model.SelectionMode
|
||||||
import org.jetbrains.exposed.v1.core.eq
|
import org.jetbrains.exposed.v1.core.eq
|
||||||
import org.jetbrains.exposed.v1.jdbc.Database
|
import org.jetbrains.exposed.v1.jdbc.Database
|
||||||
import org.jetbrains.exposed.v1.jdbc.SchemaUtils
|
import org.jetbrains.exposed.v1.jdbc.SchemaUtils
|
||||||
|
|
@ -22,7 +25,7 @@ class SectorService(private val database: Database) {
|
||||||
|
|
||||||
fun init() {
|
fun init() {
|
||||||
transaction(database) {
|
transaction(database) {
|
||||||
SchemaUtils.createMissingTablesAndColumns(SectorsTable)
|
SchemaUtils.createMissingTablesAndColumns(SectorsTable, SectorRangesTable)
|
||||||
SectorsTable.selectAll().forEach {
|
SectorsTable.selectAll().forEach {
|
||||||
val id = it[SectorsTable.id]
|
val id = it[SectorsTable.id]
|
||||||
val sector = Sector(
|
val sector = Sector(
|
||||||
|
|
@ -64,6 +67,22 @@ class SectorService(private val database: Database) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getSector(id: Int): Sector? {
|
||||||
|
return sectorsCache[id] ?: transaction(database) {
|
||||||
|
SectorsTable.selectAll().andWhere { SectorsTable.id eq id }.singleOrNull()?.let {
|
||||||
|
Sector(
|
||||||
|
it[SectorsTable.id],
|
||||||
|
it[SectorsTable.ownerActorId],
|
||||||
|
it[SectorsTable.world],
|
||||||
|
it[SectorsTable.x],
|
||||||
|
it[SectorsTable.y],
|
||||||
|
it[SectorsTable.z],
|
||||||
|
it[SectorsTable.hp]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun getHealth(id: Int): Int? {
|
fun getHealth(id: Int): Int? {
|
||||||
// Try get from cache
|
// Try get from cache
|
||||||
var currentHp = hpCache[id]
|
var currentHp = hpCache[id]
|
||||||
|
|
@ -119,6 +138,7 @@ class SectorService(private val database: Database) {
|
||||||
val sector = transaction(database) {
|
val sector = transaction(database) {
|
||||||
val record = SectorsTable.selectAll().andWhere { SectorsTable.id eq id }.singleOrNull() ?: return@transaction null
|
val record = SectorsTable.selectAll().andWhere { SectorsTable.id eq id }.singleOrNull() ?: return@transaction null
|
||||||
|
|
||||||
|
SectorRangesTable.deleteWhere { SectorRangesTable.sectorId eq id }
|
||||||
SectorsTable.deleteWhere { SectorsTable.id eq id }
|
SectorsTable.deleteWhere { SectorsTable.id eq id }
|
||||||
|
|
||||||
Sector(
|
Sector(
|
||||||
|
|
@ -211,4 +231,42 @@ class SectorService(private val database: Database) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun addRange(sectorId: Int, mode: SelectionMode, x1: Int, y1: Int, z1: Int, x2: Int, y2: Int, z2: Int, isSneaking: Boolean): SectorRange {
|
||||||
|
return transaction(database) {
|
||||||
|
val id = SectorRangesTable.insert {
|
||||||
|
it[SectorRangesTable.sectorId] = sectorId
|
||||||
|
it[SectorRangesTable.type] = mode.name
|
||||||
|
it[SectorRangesTable.x1] = x1
|
||||||
|
it[SectorRangesTable.y1] = y1
|
||||||
|
it[SectorRangesTable.z1] = z1
|
||||||
|
it[SectorRangesTable.x2] = x2
|
||||||
|
it[SectorRangesTable.y2] = y2
|
||||||
|
it[SectorRangesTable.z2] = z2
|
||||||
|
it[SectorRangesTable.isSneaking] = isSneaking
|
||||||
|
}[SectorRangesTable.id]
|
||||||
|
|
||||||
|
SectorRange(id, sectorId, mode, x1, y1, z1, x2, y2, z2, isSneaking)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getRanges(sectorId: Int): List<SectorRange> {
|
||||||
|
return transaction(database) {
|
||||||
|
SectorRangesTable.selectAll().andWhere { SectorRangesTable.sectorId eq sectorId }.map {
|
||||||
|
SectorRange(
|
||||||
|
it[SectorRangesTable.id],
|
||||||
|
it[SectorRangesTable.sectorId],
|
||||||
|
SelectionMode.valueOf(it[SectorRangesTable.type]),
|
||||||
|
it[SectorRangesTable.x1],
|
||||||
|
it[SectorRangesTable.y1],
|
||||||
|
it[SectorRangesTable.z1],
|
||||||
|
it[SectorRangesTable.x2],
|
||||||
|
it[SectorRangesTable.y2],
|
||||||
|
it[SectorRangesTable.z2],
|
||||||
|
it[SectorRangesTable.isSneaking]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
package net.hareworks.hcu.landsector.service
|
||||||
|
|
||||||
|
import net.hareworks.hcu.landsector.model.SelectionData
|
||||||
|
import org.bukkit.Location
|
||||||
|
import java.util.UUID
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.PI
|
||||||
|
import kotlin.math.ceil
|
||||||
|
|
||||||
|
class SelectionService {
|
||||||
|
private val selections = ConcurrentHashMap<UUID, SelectionData>()
|
||||||
|
|
||||||
|
fun getSelection(uuid: UUID): SelectionData {
|
||||||
|
return selections.computeIfAbsent(uuid) { SelectionData() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearSelection(uuid: UUID) {
|
||||||
|
selections.remove(uuid)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAreaDetails(uuid: UUID): String {
|
||||||
|
val data = getSelection(uuid)
|
||||||
|
if (data.point1 == null || data.point2 == null) {
|
||||||
|
return "No complete selection."
|
||||||
|
}
|
||||||
|
|
||||||
|
val p1 = data.point1!!
|
||||||
|
val p2 = data.point2!!
|
||||||
|
|
||||||
|
return if (data.mode == net.hareworks.hcu.landsector.model.SelectionMode.CUBOID) {
|
||||||
|
val width: Int
|
||||||
|
val height: Int
|
||||||
|
val length: Int
|
||||||
|
|
||||||
|
if (!data.p1Sneaking) {
|
||||||
|
// Normal: P1 to P2
|
||||||
|
width = abs(p1.blockX - p2.blockX) + 1
|
||||||
|
height = abs(p1.blockY - p2.blockY) + 1
|
||||||
|
length = abs(p1.blockZ - p2.blockZ) + 1
|
||||||
|
} else {
|
||||||
|
// Sneak: P1 is center
|
||||||
|
val dx = abs(p1.blockX - p2.blockX)
|
||||||
|
val dy = abs(p1.blockY - p2.blockY)
|
||||||
|
val dz = abs(p1.blockZ - p2.blockZ)
|
||||||
|
width = dx * 2 + 1
|
||||||
|
height = dy * 2 + 1
|
||||||
|
length = dz * 2 + 1
|
||||||
|
}
|
||||||
|
"Cuboid: ${width}x${height}x${length} (${width * height * length} blocks)"
|
||||||
|
} else {
|
||||||
|
// Cylinder
|
||||||
|
val dx = p1.blockX - p2.blockX
|
||||||
|
val dz = p1.blockZ - p2.blockZ
|
||||||
|
// Use ceil(dist - 0.5) to find the minimum integer radius R such that (dist <= R + 0.5)
|
||||||
|
// This ensures the point is included in the R+0.5 boundary without overshooting to the next integer.
|
||||||
|
val dist = Math.sqrt((dx * dx + dz * dz).toDouble())
|
||||||
|
val baseRadius = ceil(dist - 0.5)
|
||||||
|
val radius = baseRadius + 0.5
|
||||||
|
|
||||||
|
val totalHeight = if (!data.p1Sneaking) {
|
||||||
|
// Normal: P1 center, symmetric height
|
||||||
|
val h = abs(p1.blockY - p2.blockY)
|
||||||
|
h * 2 + 1
|
||||||
|
} else {
|
||||||
|
// Sneak: P1 base, P2 top
|
||||||
|
abs(p1.blockY - p2.blockY) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
val volume = PI * radius * radius * totalHeight
|
||||||
|
"Cylinder: r=${"%.0f".format(baseRadius)}, h=$totalHeight (~${ceil(volume).toInt()} blocks)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCost(uuid: UUID): Double {
|
||||||
|
val data = getSelection(uuid)
|
||||||
|
if (data.point1 == null || data.point2 == null) {
|
||||||
|
return 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Placeholder cost logic: 1 block = 1 unit?
|
||||||
|
// Need to refine based on actual volume
|
||||||
|
val p1 = data.point1!!
|
||||||
|
val p2 = data.point2!!
|
||||||
|
|
||||||
|
val volume = if (data.mode == net.hareworks.hcu.landsector.model.SelectionMode.CUBOID) {
|
||||||
|
val width = abs(p1.blockX - p2.blockX) + 1
|
||||||
|
val height = abs(p1.blockY - p2.blockY) + 1
|
||||||
|
val length = abs(p1.blockZ - p2.blockZ) + 1
|
||||||
|
(width * height * length).toDouble()
|
||||||
|
} else {
|
||||||
|
val dx = p1.blockX - p2.blockX
|
||||||
|
val dz = p1.blockZ - p2.blockZ
|
||||||
|
val dist = Math.sqrt((dx * dx + dz * dz).toDouble())
|
||||||
|
val baseRadius = ceil(dist - 0.5)
|
||||||
|
val radius = baseRadius + 0.5
|
||||||
|
val height = abs(p1.blockY - p2.blockY) + 1
|
||||||
|
PI * radius * radius * height
|
||||||
|
}
|
||||||
|
|
||||||
|
return volume * 0.5 // Example rate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,195 @@
|
||||||
|
package net.hareworks.hcu.landsector.task
|
||||||
|
|
||||||
|
import net.hareworks.hcu.landsector.model.SelectionMode
|
||||||
|
import net.hareworks.hcu.landsector.service.SelectionService
|
||||||
|
import net.hareworks.hcu.visualizer.GeometryVisualizer
|
||||||
|
import net.kyori.adventure.text.Component
|
||||||
|
import net.kyori.adventure.text.format.NamedTextColor
|
||||||
|
import org.bukkit.Bukkit
|
||||||
|
import org.bukkit.Color
|
||||||
|
import org.bukkit.Material
|
||||||
|
import org.bukkit.NamespacedKey
|
||||||
|
import org.bukkit.entity.Player
|
||||||
|
import org.bukkit.inventory.EquipmentSlot
|
||||||
|
import org.bukkit.persistence.PersistentDataType
|
||||||
|
import org.bukkit.plugin.java.JavaPlugin
|
||||||
|
import org.bukkit.scheduler.BukkitRunnable
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
class SelectionVisualizerTask(
|
||||||
|
private val plugin: JavaPlugin,
|
||||||
|
private val selectionService: SelectionService
|
||||||
|
) : BukkitRunnable() {
|
||||||
|
|
||||||
|
override fun run() {
|
||||||
|
for (player in Bukkit.getOnlinePlayers()) {
|
||||||
|
visualize(player)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun visualize(player: Player) {
|
||||||
|
// Check if holding tool
|
||||||
|
// Check if tool is in inventory
|
||||||
|
val key = NamespacedKey(plugin, "component")
|
||||||
|
val hasTool = player.inventory.contents.any { item ->
|
||||||
|
item != null && item.type == Material.FLINT &&
|
||||||
|
item.itemMeta?.persistentDataContainer?.get(key, PersistentDataType.STRING) == "land_sector_tool"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasTool) return
|
||||||
|
|
||||||
|
val selection = selectionService.getSelection(player.uniqueId)
|
||||||
|
val p1 = selection.point1 ?: return
|
||||||
|
|
||||||
|
// Determine P2: Either set in selection, or dynamic based on cursor if not set (or we show review for both?)
|
||||||
|
// If p2 is set, visualize that.
|
||||||
|
// If p2 is NOT set, visualize dynamic preview using target block.
|
||||||
|
|
||||||
|
var p2 = selection.point2
|
||||||
|
var isDynamic = false
|
||||||
|
|
||||||
|
if (p2 == null) {
|
||||||
|
val target = player.getTargetBlockExact(30)
|
||||||
|
if (target != null) {
|
||||||
|
p2 = target.location
|
||||||
|
isDynamic = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p2 == null) return // No p2 and no target
|
||||||
|
|
||||||
|
// If different world, ignore
|
||||||
|
if (p1.world != p2.world) return
|
||||||
|
|
||||||
|
if (selection.mode == SelectionMode.CUBOID) {
|
||||||
|
// Cuboid visualization
|
||||||
|
var minX: Double
|
||||||
|
var minY: Double
|
||||||
|
var minZ: Double
|
||||||
|
var maxX: Double
|
||||||
|
var maxY: Double
|
||||||
|
var maxZ: Double
|
||||||
|
|
||||||
|
if (!selection.p1Sneaking) {
|
||||||
|
// Normal: corner to corner
|
||||||
|
minX = minOf(p1.blockX, p2.blockX).toDouble()
|
||||||
|
minY = minOf(p1.blockY, p2.blockY).toDouble()
|
||||||
|
minZ = minOf(p1.blockZ, p2.blockZ).toDouble()
|
||||||
|
maxX = maxOf(p1.blockX, p2.blockX).toDouble() + 1.0
|
||||||
|
maxY = maxOf(p1.blockY, p2.blockY).toDouble() + 1.0
|
||||||
|
maxZ = maxOf(p1.blockZ, p2.blockZ).toDouble() + 1.0
|
||||||
|
} else {
|
||||||
|
// Sneak: P1 center, P2 defines radius
|
||||||
|
val dx = abs(p1.blockX - p2.blockX)
|
||||||
|
val dy = abs(p1.blockY - p2.blockY)
|
||||||
|
val dz = abs(p1.blockZ - p2.blockZ)
|
||||||
|
|
||||||
|
minX = (p1.blockX - dx).toDouble()
|
||||||
|
maxX = (p1.blockX + dx).toDouble() + 1.0
|
||||||
|
minY = (p1.blockY - dy).toDouble()
|
||||||
|
maxY = (p1.blockY + dy).toDouble() + 1.0
|
||||||
|
minZ = (p1.blockZ - dz).toDouble()
|
||||||
|
maxZ = (p1.blockZ + dz).toDouble() + 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
GeometryVisualizer.drawCuboid(player, minX, minY, minZ, maxX, maxY, maxZ)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Cylinder visualization
|
||||||
|
val centerX: Double
|
||||||
|
val centerY: Double
|
||||||
|
val centerZ: Double
|
||||||
|
val radius: Double
|
||||||
|
val minY: Double
|
||||||
|
val maxY: Double
|
||||||
|
|
||||||
|
val dx = p1.blockX - p2.blockX
|
||||||
|
val dz = p1.blockZ - p2.blockZ
|
||||||
|
val dist = Math.sqrt((dx * dx + dz * dz).toDouble())
|
||||||
|
radius = kotlin.math.ceil(dist - 0.5)
|
||||||
|
|
||||||
|
if (!selection.p1Sneaking) {
|
||||||
|
// Normal: P1 center, symmetric height based on P2.y diff
|
||||||
|
centerX = p1.blockX + 0.5
|
||||||
|
centerY = p1.blockY + 0.5 // Logic center
|
||||||
|
centerZ = p1.blockZ + 0.5
|
||||||
|
|
||||||
|
val hDiff = abs(p1.blockY - p2.blockY)
|
||||||
|
// Bottom and Top
|
||||||
|
// From center block center, go down hDiff blocks (and include center block?)
|
||||||
|
// If P1.y = 10, P2.y = 12. Diff 2.
|
||||||
|
// Blocks: 8, 9, 10, 11, 12. Range [8, 12]. Height 5. (2*2 + 1)
|
||||||
|
|
||||||
|
// Block coords:
|
||||||
|
val baseBlockY = p1.blockY - hDiff
|
||||||
|
val topBlockY = p1.blockY + hDiff
|
||||||
|
|
||||||
|
minY = baseBlockY.toDouble()
|
||||||
|
maxY = topBlockY.toDouble() + 1.0
|
||||||
|
} else {
|
||||||
|
// Sneak: P1 base center. P2 top center.
|
||||||
|
centerX = p1.blockX + 0.5
|
||||||
|
centerZ = p1.blockZ + 0.5
|
||||||
|
// Only height from p2
|
||||||
|
val y1 = p1.blockY
|
||||||
|
val y2 = p2.blockY
|
||||||
|
|
||||||
|
val baseBlockY = minOf(y1, y2)
|
||||||
|
val topBlockY = maxOf(y1, y2)
|
||||||
|
|
||||||
|
minY = baseBlockY.toDouble()
|
||||||
|
maxY = topBlockY.toDouble() + 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate blocks for cylinder outline
|
||||||
|
val cX = kotlin.math.floor(centerX).toInt()
|
||||||
|
val cZ = kotlin.math.floor(centerZ).toInt()
|
||||||
|
val blocks = mutableSetOf<Pair<Int, Int>>()
|
||||||
|
// Add 0.5 to radius to encompass the full block width of the boundary blocks
|
||||||
|
val actualRadius = radius + 0.5
|
||||||
|
val radiusSq = actualRadius * actualRadius
|
||||||
|
val rInt = actualRadius.toInt() + 1
|
||||||
|
|
||||||
|
for (dx in -rInt..rInt) {
|
||||||
|
for (dz in -rInt..rInt) {
|
||||||
|
// Check if block center is within the radius
|
||||||
|
if (dx * dx + dz * dz <= radiusSq) {
|
||||||
|
blocks.add(Pair(cX + dx, cZ + dz))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Draw wireframe circle (smooth) layered with block outline
|
||||||
|
// Wireframe uses the expanded radius to match the block outline visuals
|
||||||
|
GeometryVisualizer.drawCylinder(player, centerX, p1.blockY.toDouble(), centerZ, radius.toInt(), minY, maxY)
|
||||||
|
|
||||||
|
val color = Color.fromRGB(100, 200, 255)
|
||||||
|
// Draw bottom surface outline
|
||||||
|
GeometryVisualizer.drawBlockSurfaceOutline(
|
||||||
|
player,
|
||||||
|
minY,
|
||||||
|
blocks,
|
||||||
|
{ _, _, _ -> false },
|
||||||
|
color,
|
||||||
|
minY
|
||||||
|
)
|
||||||
|
|
||||||
|
// Draw top surface outline
|
||||||
|
GeometryVisualizer.drawBlockSurfaceOutline(
|
||||||
|
player,
|
||||||
|
maxY,
|
||||||
|
blocks,
|
||||||
|
{ _, _, _ -> false },
|
||||||
|
color,
|
||||||
|
maxY
|
||||||
|
)
|
||||||
|
|
||||||
|
// Vertical edges for the outline?
|
||||||
|
// The current generic visualizer doesn't have a specific "drawVerticalConnectors" for this map-based approach easily accessible
|
||||||
|
// or we'd have to iterate edges.
|
||||||
|
// For now, surface outlines should be sufficient as requested.
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user