diff --git a/Lands b/Lands index 0f432b0..d256db9 160000 --- a/Lands +++ b/Lands @@ -1 +1 @@ -Subproject commit 0f432b0e89614bc07bd710423284da5e0228e25a +Subproject commit d256db9f518bb2a6444a36f71b69afee783fd077 diff --git a/bin/main/net/hareworks/hcu/landsector/LandSectorPlugin.kt b/bin/main/net/hareworks/hcu/landsector/LandSectorPlugin.kt index e0e7640..1ae9756 100644 --- a/bin/main/net/hareworks/hcu/landsector/LandSectorPlugin.kt +++ b/bin/main/net/hareworks/hcu/landsector/LandSectorPlugin.kt @@ -75,7 +75,7 @@ class LandSectorPlugin : JavaPlugin() { 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) + net.hareworks.hcu.landsector.task.SelectionVisualizerTask(this, selService, service).runTaskTimer(this, 0L, 4L) } else { logger.severe("SelectionService not initialized!") } diff --git a/bin/main/net/hareworks/hcu/landsector/listener/SectorListener.kt b/bin/main/net/hareworks/hcu/landsector/listener/SectorListener.kt index 459fb19..b2bdb2b 100644 --- a/bin/main/net/hareworks/hcu/landsector/listener/SectorListener.kt +++ b/bin/main/net/hareworks/hcu/landsector/listener/SectorListener.kt @@ -303,13 +303,21 @@ class SectorListener( } else { content.append(Component.text("Parts:", NamedTextColor.BLACK)) ranges.forEach { range -> - content.append(Component.text("\n- [${range.id}] ${range.type}", NamedTextColor.DARK_GRAY)) - content.append(Component.text(" ")) + content.append(Component.text("\n")) content.append( - Component.text("[x]", NamedTextColor.RED) + Component.text("[x] ", NamedTextColor.RED) .clickEvent(ClickEvent.runCommand("/landsector range delete $sectorId ${range.id}")) .hoverEvent(net.kyori.adventure.text.event.HoverEvent.showText(Component.text("Click to delete range"))) ) + + val info = if (range.type == net.hareworks.hcu.landsector.model.SelectionMode.CUBOID) { + "(${range.x1},${range.y1},${range.z1})~(${range.x2},${range.y2},${range.z2})" + } else { + // Cylinder: x1,y1,z1 is Center. x2=R, y2=Bottom, z2=Top + val h = 1 + range.y2 + range.z2 + "Cyl @(${range.x1},${range.y1},${range.z1}) R:${range.x2} H:$h" + } + content.append(Component.text(info, NamedTextColor.DARK_GRAY)) } } diff --git a/bin/main/net/hareworks/hcu/landsector/listener/SelectionListener.kt b/bin/main/net/hareworks/hcu/landsector/listener/SelectionListener.kt index a2f3546..e9ce31c 100644 --- a/bin/main/net/hareworks/hcu/landsector/listener/SelectionListener.kt +++ b/bin/main/net/hareworks/hcu/landsector/listener/SelectionListener.kt @@ -37,43 +37,38 @@ class SelectionListener( val player = event.player val selection = selectionService.getSelection(player.uniqueId) - // Left Click: Switch Mode + // Left Click: Cancel only 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 + event.isCancelled = true return } // Right Click: Set Points - if (event.action == Action.RIGHT_CLICK_BLOCK) { - event.isCancelled = true // Prevent placing + if (event.action == Action.RIGHT_CLICK_BLOCK || event.action == Action.RIGHT_CLICK_AIR) { + event.isCancelled = true // Prevent placing or item usage - val clickedBlock = event.clickedBlock ?: return - val loc = clickedBlock.location + val targetBlock = event.clickedBlock ?: player.rayTraceBlocks(100.0)?.hitBlock + + if (targetBlock == null) return + + val loc = targetBlock.location if (selection.point1 == null) { // Set Point 1 selection.point1 = loc - selection.p1Sneaking = player.isSneaking - selection.point2 = null // Clear p2 just in case + // selection.isCenterMode is persistent, so we don't set it here based on sneaking + selection.point2 = null - player.sendMessage(Component.text("Position 1 set at ${loc.blockX}, ${loc.blockY}, ${loc.blockZ} (Mode: ${selection.mode}, Sneaking: ${selection.p1Sneaking})", NamedTextColor.GREEN)) + player.sendMessage(Component.text("Position 1 set at ${loc.blockX}, ${loc.blockY}, ${loc.blockZ} (Mode: ${selection.mode}, CenterMode: ${selection.isCenterMode})", NamedTextColor.GREEN)) + player.playSound(player.location, org.bukkit.Sound.UI_BUTTON_CLICK, 1.0f, 2.0f) } 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)) + player.sendMessage(Component.text("Selection reset. Position 1 set at ${loc.blockX}, ${loc.blockY}, ${loc.blockZ} (Mode: ${selection.mode}, CenterMode: ${selection.isCenterMode})", NamedTextColor.GREEN)) + player.playSound(player.location, org.bukkit.Sound.BLOCK_NOTE_BLOCK_BASS, 1.0f, 0.5f) } else { selection.point2 = loc // Check for sector ID @@ -90,10 +85,11 @@ class SelectionListener( selection.mode, p1.blockX, p1.blockY, p1.blockZ, p2.blockX, p2.blockY, p2.blockZ, - selection.p1Sneaking + selection.isCenterMode ) player.sendMessage(Component.text("Range added to Sector #$sId.", NamedTextColor.GREEN)) + player.playSound(player.location, org.bukkit.Sound.ENTITY_PLAYER_LEVELUP, 1.0f, 2.0f) // Clear selection selection.point1 = null @@ -104,6 +100,8 @@ class SelectionListener( // Show Cost / Area player.sendMessage(Component.text(selectionService.getAreaDetails(player.uniqueId), NamedTextColor.AQUA)) player.sendMessage(Component.text("Cost: ${selectionService.getCost(player.uniqueId)}", NamedTextColor.GOLD)) + + player.playSound(player.location, org.bukkit.Sound.UI_BUTTON_CLICK, 1.0f, 1.0f) } } } @@ -116,7 +114,25 @@ class SelectionListener( 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)) + + val player = event.player + val selection = selectionService.getSelection(player.uniqueId) + + if (player.isSneaking) { + // Toggle Center/Corner Mode + selection.isCenterMode = !selection.isCenterMode + player.sendMessage(Component.text("Selection Modifier: ${if (selection.isCenterMode) "Center/Base Mode" else "Corner/Symmetric Mode"}", NamedTextColor.GOLD)) + player.playSound(player.location, org.bukkit.Sound.BLOCK_COMPARATOR_CLICK, 1.0f, 1.0f) + } else { + // Toggle Shape Mode + selection.mode = if (selection.mode == SelectionMode.CUBOID) SelectionMode.CYLINDER else SelectionMode.CUBOID + player.sendMessage(Component.text("Shape: ${selection.mode}", NamedTextColor.YELLOW)) + player.playSound(player.location, org.bukkit.Sound.ITEM_BOOK_PAGE_TURN, 1.0f, 1.0f) + + // Reset selection on mode switch + selection.point1 = null + selection.point2 = null + } } } diff --git a/bin/main/net/hareworks/hcu/landsector/model/SelectionData.kt b/bin/main/net/hareworks/hcu/landsector/model/SelectionData.kt index 9ca2428..7e2e840 100644 --- a/bin/main/net/hareworks/hcu/landsector/model/SelectionData.kt +++ b/bin/main/net/hareworks/hcu/landsector/model/SelectionData.kt @@ -11,5 +11,5 @@ data class SelectionData( var mode: SelectionMode = SelectionMode.CUBOID, var point1: Location? = null, var point2: Location? = null, - var p1Sneaking: Boolean = false + var isCenterMode: Boolean = false // Replaces old p1Sneaking logic with explicit toggle ) diff --git a/bin/main/net/hareworks/hcu/landsector/service/SectorService.kt b/bin/main/net/hareworks/hcu/landsector/service/SectorService.kt index d842281..b83205d 100644 --- a/bin/main/net/hareworks/hcu/landsector/service/SectorService.kt +++ b/bin/main/net/hareworks/hcu/landsector/service/SectorService.kt @@ -2,10 +2,11 @@ package net.hareworks.hcu.landsector.service import net.hareworks.hcu.landsector.database.SectorDraftsTable import net.hareworks.hcu.landsector.database.SectorsTable -// SectorRangesTable removed import net.hareworks.hcu.landsector.model.Sector import net.hareworks.hcu.landsector.model.SectorRange import net.hareworks.hcu.landsector.model.SelectionMode +import net.hareworks.hcu.lands.model.LandData +import net.hareworks.hcu.lands.model.Shape import org.jetbrains.exposed.v1.core.eq import org.jetbrains.exposed.v1.jdbc.Database import org.jetbrains.exposed.v1.jdbc.SchemaUtils @@ -385,7 +386,7 @@ class SectorService(private val database: Database, private val landService: net sectorId, SelectionMode.CYLINDER, shape.x, shape.y, shape.z, - shape.x, shape.y, shape.z, + shape.radius.toInt(), shape.bottomHeight, shape.topHeight, false ) } @@ -395,6 +396,17 @@ class SectorService(private val database: Database, private val landService: net } } + fun getSectorShapes(sectorId: Int): List { + val sector = getSector(sectorId) ?: return emptyList() + + val land = if (sector.landId != null) { + landService.getLand(sector.landId) + } else { + draftLands[sectorId] + } ?: return emptyList() + + return land.data.parts + } fun deleteRange(sectorId: Int, rangeIndex: Int): Boolean { val sector = getSector(sectorId) ?: return false if (sector.landId != null) return false // Cannot edit confirmed @@ -470,3 +482,5 @@ class SectorService(private val database: Database, private val landService: net } } } + + diff --git a/bin/main/net/hareworks/hcu/landsector/service/SelectionService.kt b/bin/main/net/hareworks/hcu/landsector/service/SelectionService.kt index ad95dc2..f79b08f 100644 --- a/bin/main/net/hareworks/hcu/landsector/service/SelectionService.kt +++ b/bin/main/net/hareworks/hcu/landsector/service/SelectionService.kt @@ -33,13 +33,13 @@ class SelectionService { val height: Int val length: Int - if (!data.p1Sneaking) { + if (!data.isCenterMode) { // 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 + // Center Mode: P1 is center val dx = abs(p1.blockX - p2.blockX) val dy = abs(p1.blockY - p2.blockY) val dz = abs(p1.blockZ - p2.blockZ) @@ -58,12 +58,12 @@ class SelectionService { val baseRadius = ceil(dist - 0.5) val radius = baseRadius + 0.5 - val totalHeight = if (!data.p1Sneaking) { + val totalHeight = if (!data.isCenterMode) { // Normal: P1 center, symmetric height val h = abs(p1.blockY - p2.blockY) h * 2 + 1 } else { - // Sneak: P1 base, P2 top + // Center Mode: P1 base, P2 top (or vice versa, height is diff) abs(p1.blockY - p2.blockY) + 1 } diff --git a/bin/main/net/hareworks/hcu/landsector/task/SelectionVisualizerTask.kt b/bin/main/net/hareworks/hcu/landsector/task/SelectionVisualizerTask.kt index e8b57e8..0074d36 100644 --- a/bin/main/net/hareworks/hcu/landsector/task/SelectionVisualizerTask.kt +++ b/bin/main/net/hareworks/hcu/landsector/task/SelectionVisualizerTask.kt @@ -2,6 +2,7 @@ package net.hareworks.hcu.landsector.task import net.hareworks.hcu.landsector.model.SelectionMode import net.hareworks.hcu.landsector.service.SelectionService +import net.hareworks.hcu.landsector.service.SectorService import net.hareworks.hcu.visualizer.GeometryVisualizer import net.kyori.adventure.text.Component import net.kyori.adventure.text.format.NamedTextColor @@ -18,7 +19,8 @@ import kotlin.math.abs class SelectionVisualizerTask( private val plugin: JavaPlugin, - private val selectionService: SelectionService + private val selectionService: SelectionService, + private val sectorService: SectorService ) : BukkitRunnable() { override fun run() { @@ -28,41 +30,107 @@ class SelectionVisualizerTask( } 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" + val sectorKey = NamespacedKey(plugin, "sector_id") + + // Find tool in inventory to get sector ID + var sectorId: Int? = null + var hasTool = false + + // 1. Check held items first (Priority: Held ID > Inventory ID) + val heldItems = listOf(player.inventory.itemInMainHand, player.inventory.itemInOffHand) + for (item in heldItems) { + if (item.type == Material.FLINT) { + val meta = item.itemMeta + if (meta != null && meta.persistentDataContainer.get(key, PersistentDataType.STRING) == "land_sector_tool") { + hasTool = true + if (meta.persistentDataContainer.has(sectorKey, PersistentDataType.INTEGER)) { + sectorId = meta.persistentDataContainer.get(sectorKey, PersistentDataType.INTEGER) + break // Found specific sector tool in hand, prioritize this. + } + } + } + } + + // 2. If no specific sector ID found in hand, scan inventory for any linked tool + if (sectorId == null) { + for (item in player.inventory.contents) { + if (item != null && item.type == Material.FLINT) { + val meta = item.itemMeta ?: continue + if (meta.persistentDataContainer.get(key, PersistentDataType.STRING) == "land_sector_tool") { + hasTool = true + if (meta.persistentDataContainer.has(sectorKey, PersistentDataType.INTEGER)) { + sectorId = meta.persistentDataContainer.get(sectorKey, PersistentDataType.INTEGER) + break // Found linked tool in inventory. + } + } + } + } } if (!hasTool) return + // 1. Visualize Existing Ranges (Green) + if (sectorId != null) { + // World Check + val sector = sectorService.getSector(sectorId) + if (sector != null && sector.world == player.world.name) { + val shapes = sectorService.getSectorShapes(sectorId) + val color = Color.fromRGB(0, 255, 0) // Green for saved + + for (shape in shapes) { + when (shape) { + is net.hareworks.hcu.lands.model.Shape.Cuboid -> { + val minX = minOf(shape.x1, shape.x2).toDouble() + val minY = minOf(shape.y1, shape.y2).toDouble() + val minZ = minOf(shape.z1, shape.z2).toDouble() + val maxX = maxOf(shape.x1, shape.x2).toDouble() + 1.0 + val maxY = maxOf(shape.y1, shape.y2).toDouble() + 1.0 + val maxZ = maxOf(shape.z1, shape.z2).toDouble() + 1.0 + + GeometryVisualizer.drawCuboid( + player, + minX, minY, minZ, + maxX, maxY, maxZ, + color + ) + } + is net.hareworks.hcu.lands.model.Shape.Cylinder -> { + val minY = (shape.y - shape.bottomHeight).toDouble() + val maxY = (shape.y + shape.topHeight + 1).toDouble() + + GeometryVisualizer.drawCylinder( + player, + shape.x + 0.5, shape.y.toDouble(), shape.z + 0.5, + shape.radius.toInt(), + minY, + maxY, + color + ) + } + } + } + } + } + + // 2. Visualize Current Selection (Blue) 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 (p2 == null) return if (p1.world != p2.world) return - + + // ... (Existing Draw Logic for Selection) ... + // ... (Existing Draw Logic for Selection) ... if (selection.mode == SelectionMode.CUBOID) { - // Cuboid visualization var minX: Double var minY: Double var minZ: Double @@ -70,8 +138,8 @@ class SelectionVisualizerTask( var maxY: Double var maxZ: Double - if (!selection.p1Sneaking) { - // Normal: corner to corner + if (!selection.isCenterMode) { + // Normal: Corner to Corner (Diagonal) minX = minOf(p1.blockX, p2.blockX).toDouble() minY = minOf(p1.blockY, p2.blockY).toDouble() minZ = minOf(p1.blockZ, p2.blockZ).toDouble() @@ -79,7 +147,7 @@ class SelectionVisualizerTask( 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 + // Center Mode: P1 is Center val dx = abs(p1.blockX - p2.blockX) val dy = abs(p1.blockY - p2.blockY) val dz = abs(p1.blockZ - p2.blockZ) @@ -95,9 +163,8 @@ class SelectionVisualizerTask( GeometryVisualizer.drawCuboid(player, minX, minY, minZ, maxX, maxY, maxZ) } else { - // Cylinder visualization val centerX: Double - val centerY: Double + val centerY: Double // This variable is actually unused in the logic below if we override args, but kept for clarity if needed val centerZ: Double val radius: Double val minY: Double @@ -108,88 +175,55 @@ class SelectionVisualizerTask( 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 + if (!selection.isCenterMode) { + // Normal: P1 is Center, Symmetric Height centerX = p1.blockX + 0.5 - centerY = p1.blockY + 0.5 // Logic center + centerY = p1.blockY + 0.5 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. + // Center/Base Mode: P1 is Base, P2 defines Height/Radius 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>() - // 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. + // Draw Cylinder + GeometryVisualizer.drawCylinder(player, centerX, p1.blockY.toDouble(), centerZ, radius.toInt(), minY, maxY) + + // Draw Outlines (Surface) + val cX = kotlin.math.floor(centerX).toInt() + val cZ = kotlin.math.floor(centerZ).toInt() + val blocks = mutableSetOf>() + val actualRadius = radius + 0.5 + val radiusSq = actualRadius * actualRadius + val rInt = actualRadius.toInt() + 1 + + for (dx in -rInt..rInt) { + for (dz in -rInt..rInt) { + if (dx * dx + dz * dz <= radiusSq) { + blocks.add(Pair(cX + dx, cZ + dz)) + } + } + } + + val color = Color.fromRGB(100, 200, 255) + GeometryVisualizer.drawBlockSurfaceOutline(player, minY, blocks, { _, _, _ -> false }, color, minY) + GeometryVisualizer.drawBlockSurfaceOutline(player, maxY, blocks, { _, _, _ -> false }, color, maxY) } - } } +// Private helper extensions if needed for Shape +private fun net.hareworks.hcu.lands.model.Shape.worldName(): String { return "" } // Dummy, not used because we can't easily check + diff --git a/build.gradle.kts b/build.gradle.kts index adfcc59..a1ca2c7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -30,7 +30,7 @@ dependencies { compileOnly("com.michael-bull.kotlin-result:kotlin-result:2.1.0") compileOnly("net.hareworks:kommand-lib:1.1") compileOnly("net.hareworks:permits-lib") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0") + compileOnly("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0") // Visualizer Lib (Bundled & Relocated) implementation("net.hareworks.hcu:visualizer-lib:1.0") diff --git a/src/main/kotlin/net/hareworks/hcu/landsector/LandSectorPlugin.kt b/src/main/kotlin/net/hareworks/hcu/landsector/LandSectorPlugin.kt index e0e7640..1ae9756 100644 --- a/src/main/kotlin/net/hareworks/hcu/landsector/LandSectorPlugin.kt +++ b/src/main/kotlin/net/hareworks/hcu/landsector/LandSectorPlugin.kt @@ -75,7 +75,7 @@ class LandSectorPlugin : JavaPlugin() { 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) + net.hareworks.hcu.landsector.task.SelectionVisualizerTask(this, selService, service).runTaskTimer(this, 0L, 4L) } else { logger.severe("SelectionService not initialized!") } diff --git a/src/main/kotlin/net/hareworks/hcu/landsector/listener/SectorListener.kt b/src/main/kotlin/net/hareworks/hcu/landsector/listener/SectorListener.kt index 459fb19..b2bdb2b 100644 --- a/src/main/kotlin/net/hareworks/hcu/landsector/listener/SectorListener.kt +++ b/src/main/kotlin/net/hareworks/hcu/landsector/listener/SectorListener.kt @@ -303,13 +303,21 @@ class SectorListener( } else { content.append(Component.text("Parts:", NamedTextColor.BLACK)) ranges.forEach { range -> - content.append(Component.text("\n- [${range.id}] ${range.type}", NamedTextColor.DARK_GRAY)) - content.append(Component.text(" ")) + content.append(Component.text("\n")) content.append( - Component.text("[x]", NamedTextColor.RED) + Component.text("[x] ", NamedTextColor.RED) .clickEvent(ClickEvent.runCommand("/landsector range delete $sectorId ${range.id}")) .hoverEvent(net.kyori.adventure.text.event.HoverEvent.showText(Component.text("Click to delete range"))) ) + + val info = if (range.type == net.hareworks.hcu.landsector.model.SelectionMode.CUBOID) { + "(${range.x1},${range.y1},${range.z1})~(${range.x2},${range.y2},${range.z2})" + } else { + // Cylinder: x1,y1,z1 is Center. x2=R, y2=Bottom, z2=Top + val h = 1 + range.y2 + range.z2 + "Cyl @(${range.x1},${range.y1},${range.z1}) R:${range.x2} H:$h" + } + content.append(Component.text(info, NamedTextColor.DARK_GRAY)) } } diff --git a/src/main/kotlin/net/hareworks/hcu/landsector/listener/SelectionListener.kt b/src/main/kotlin/net/hareworks/hcu/landsector/listener/SelectionListener.kt index a2f3546..e9ce31c 100644 --- a/src/main/kotlin/net/hareworks/hcu/landsector/listener/SelectionListener.kt +++ b/src/main/kotlin/net/hareworks/hcu/landsector/listener/SelectionListener.kt @@ -37,43 +37,38 @@ class SelectionListener( val player = event.player val selection = selectionService.getSelection(player.uniqueId) - // Left Click: Switch Mode + // Left Click: Cancel only 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 + event.isCancelled = true return } // Right Click: Set Points - if (event.action == Action.RIGHT_CLICK_BLOCK) { - event.isCancelled = true // Prevent placing + if (event.action == Action.RIGHT_CLICK_BLOCK || event.action == Action.RIGHT_CLICK_AIR) { + event.isCancelled = true // Prevent placing or item usage - val clickedBlock = event.clickedBlock ?: return - val loc = clickedBlock.location + val targetBlock = event.clickedBlock ?: player.rayTraceBlocks(100.0)?.hitBlock + + if (targetBlock == null) return + + val loc = targetBlock.location if (selection.point1 == null) { // Set Point 1 selection.point1 = loc - selection.p1Sneaking = player.isSneaking - selection.point2 = null // Clear p2 just in case + // selection.isCenterMode is persistent, so we don't set it here based on sneaking + selection.point2 = null - player.sendMessage(Component.text("Position 1 set at ${loc.blockX}, ${loc.blockY}, ${loc.blockZ} (Mode: ${selection.mode}, Sneaking: ${selection.p1Sneaking})", NamedTextColor.GREEN)) + player.sendMessage(Component.text("Position 1 set at ${loc.blockX}, ${loc.blockY}, ${loc.blockZ} (Mode: ${selection.mode}, CenterMode: ${selection.isCenterMode})", NamedTextColor.GREEN)) + player.playSound(player.location, org.bukkit.Sound.UI_BUTTON_CLICK, 1.0f, 2.0f) } 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)) + player.sendMessage(Component.text("Selection reset. Position 1 set at ${loc.blockX}, ${loc.blockY}, ${loc.blockZ} (Mode: ${selection.mode}, CenterMode: ${selection.isCenterMode})", NamedTextColor.GREEN)) + player.playSound(player.location, org.bukkit.Sound.BLOCK_NOTE_BLOCK_BASS, 1.0f, 0.5f) } else { selection.point2 = loc // Check for sector ID @@ -90,10 +85,11 @@ class SelectionListener( selection.mode, p1.blockX, p1.blockY, p1.blockZ, p2.blockX, p2.blockY, p2.blockZ, - selection.p1Sneaking + selection.isCenterMode ) player.sendMessage(Component.text("Range added to Sector #$sId.", NamedTextColor.GREEN)) + player.playSound(player.location, org.bukkit.Sound.ENTITY_PLAYER_LEVELUP, 1.0f, 2.0f) // Clear selection selection.point1 = null @@ -104,6 +100,8 @@ class SelectionListener( // Show Cost / Area player.sendMessage(Component.text(selectionService.getAreaDetails(player.uniqueId), NamedTextColor.AQUA)) player.sendMessage(Component.text("Cost: ${selectionService.getCost(player.uniqueId)}", NamedTextColor.GOLD)) + + player.playSound(player.location, org.bukkit.Sound.UI_BUTTON_CLICK, 1.0f, 1.0f) } } } @@ -116,7 +114,25 @@ class SelectionListener( 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)) + + val player = event.player + val selection = selectionService.getSelection(player.uniqueId) + + if (player.isSneaking) { + // Toggle Center/Corner Mode + selection.isCenterMode = !selection.isCenterMode + player.sendMessage(Component.text("Selection Modifier: ${if (selection.isCenterMode) "Center/Base Mode" else "Corner/Symmetric Mode"}", NamedTextColor.GOLD)) + player.playSound(player.location, org.bukkit.Sound.BLOCK_COMPARATOR_CLICK, 1.0f, 1.0f) + } else { + // Toggle Shape Mode + selection.mode = if (selection.mode == SelectionMode.CUBOID) SelectionMode.CYLINDER else SelectionMode.CUBOID + player.sendMessage(Component.text("Shape: ${selection.mode}", NamedTextColor.YELLOW)) + player.playSound(player.location, org.bukkit.Sound.ITEM_BOOK_PAGE_TURN, 1.0f, 1.0f) + + // Reset selection on mode switch + selection.point1 = null + selection.point2 = null + } } } diff --git a/src/main/kotlin/net/hareworks/hcu/landsector/model/SelectionData.kt b/src/main/kotlin/net/hareworks/hcu/landsector/model/SelectionData.kt index 9ca2428..7e2e840 100644 --- a/src/main/kotlin/net/hareworks/hcu/landsector/model/SelectionData.kt +++ b/src/main/kotlin/net/hareworks/hcu/landsector/model/SelectionData.kt @@ -11,5 +11,5 @@ data class SelectionData( var mode: SelectionMode = SelectionMode.CUBOID, var point1: Location? = null, var point2: Location? = null, - var p1Sneaking: Boolean = false + var isCenterMode: Boolean = false // Replaces old p1Sneaking logic with explicit toggle ) diff --git a/src/main/kotlin/net/hareworks/hcu/landsector/service/SectorService.kt b/src/main/kotlin/net/hareworks/hcu/landsector/service/SectorService.kt index d842281..b83205d 100644 --- a/src/main/kotlin/net/hareworks/hcu/landsector/service/SectorService.kt +++ b/src/main/kotlin/net/hareworks/hcu/landsector/service/SectorService.kt @@ -2,10 +2,11 @@ package net.hareworks.hcu.landsector.service import net.hareworks.hcu.landsector.database.SectorDraftsTable import net.hareworks.hcu.landsector.database.SectorsTable -// SectorRangesTable removed import net.hareworks.hcu.landsector.model.Sector import net.hareworks.hcu.landsector.model.SectorRange import net.hareworks.hcu.landsector.model.SelectionMode +import net.hareworks.hcu.lands.model.LandData +import net.hareworks.hcu.lands.model.Shape import org.jetbrains.exposed.v1.core.eq import org.jetbrains.exposed.v1.jdbc.Database import org.jetbrains.exposed.v1.jdbc.SchemaUtils @@ -385,7 +386,7 @@ class SectorService(private val database: Database, private val landService: net sectorId, SelectionMode.CYLINDER, shape.x, shape.y, shape.z, - shape.x, shape.y, shape.z, + shape.radius.toInt(), shape.bottomHeight, shape.topHeight, false ) } @@ -395,6 +396,17 @@ class SectorService(private val database: Database, private val landService: net } } + fun getSectorShapes(sectorId: Int): List { + val sector = getSector(sectorId) ?: return emptyList() + + val land = if (sector.landId != null) { + landService.getLand(sector.landId) + } else { + draftLands[sectorId] + } ?: return emptyList() + + return land.data.parts + } fun deleteRange(sectorId: Int, rangeIndex: Int): Boolean { val sector = getSector(sectorId) ?: return false if (sector.landId != null) return false // Cannot edit confirmed @@ -470,3 +482,5 @@ class SectorService(private val database: Database, private val landService: net } } } + + diff --git a/src/main/kotlin/net/hareworks/hcu/landsector/service/SelectionService.kt b/src/main/kotlin/net/hareworks/hcu/landsector/service/SelectionService.kt index ad95dc2..f79b08f 100644 --- a/src/main/kotlin/net/hareworks/hcu/landsector/service/SelectionService.kt +++ b/src/main/kotlin/net/hareworks/hcu/landsector/service/SelectionService.kt @@ -33,13 +33,13 @@ class SelectionService { val height: Int val length: Int - if (!data.p1Sneaking) { + if (!data.isCenterMode) { // 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 + // Center Mode: P1 is center val dx = abs(p1.blockX - p2.blockX) val dy = abs(p1.blockY - p2.blockY) val dz = abs(p1.blockZ - p2.blockZ) @@ -58,12 +58,12 @@ class SelectionService { val baseRadius = ceil(dist - 0.5) val radius = baseRadius + 0.5 - val totalHeight = if (!data.p1Sneaking) { + val totalHeight = if (!data.isCenterMode) { // Normal: P1 center, symmetric height val h = abs(p1.blockY - p2.blockY) h * 2 + 1 } else { - // Sneak: P1 base, P2 top + // Center Mode: P1 base, P2 top (or vice versa, height is diff) abs(p1.blockY - p2.blockY) + 1 } diff --git a/src/main/kotlin/net/hareworks/hcu/landsector/task/SelectionVisualizerTask.kt b/src/main/kotlin/net/hareworks/hcu/landsector/task/SelectionVisualizerTask.kt index e8b57e8..0074d36 100644 --- a/src/main/kotlin/net/hareworks/hcu/landsector/task/SelectionVisualizerTask.kt +++ b/src/main/kotlin/net/hareworks/hcu/landsector/task/SelectionVisualizerTask.kt @@ -2,6 +2,7 @@ package net.hareworks.hcu.landsector.task import net.hareworks.hcu.landsector.model.SelectionMode import net.hareworks.hcu.landsector.service.SelectionService +import net.hareworks.hcu.landsector.service.SectorService import net.hareworks.hcu.visualizer.GeometryVisualizer import net.kyori.adventure.text.Component import net.kyori.adventure.text.format.NamedTextColor @@ -18,7 +19,8 @@ import kotlin.math.abs class SelectionVisualizerTask( private val plugin: JavaPlugin, - private val selectionService: SelectionService + private val selectionService: SelectionService, + private val sectorService: SectorService ) : BukkitRunnable() { override fun run() { @@ -28,41 +30,107 @@ class SelectionVisualizerTask( } 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" + val sectorKey = NamespacedKey(plugin, "sector_id") + + // Find tool in inventory to get sector ID + var sectorId: Int? = null + var hasTool = false + + // 1. Check held items first (Priority: Held ID > Inventory ID) + val heldItems = listOf(player.inventory.itemInMainHand, player.inventory.itemInOffHand) + for (item in heldItems) { + if (item.type == Material.FLINT) { + val meta = item.itemMeta + if (meta != null && meta.persistentDataContainer.get(key, PersistentDataType.STRING) == "land_sector_tool") { + hasTool = true + if (meta.persistentDataContainer.has(sectorKey, PersistentDataType.INTEGER)) { + sectorId = meta.persistentDataContainer.get(sectorKey, PersistentDataType.INTEGER) + break // Found specific sector tool in hand, prioritize this. + } + } + } + } + + // 2. If no specific sector ID found in hand, scan inventory for any linked tool + if (sectorId == null) { + for (item in player.inventory.contents) { + if (item != null && item.type == Material.FLINT) { + val meta = item.itemMeta ?: continue + if (meta.persistentDataContainer.get(key, PersistentDataType.STRING) == "land_sector_tool") { + hasTool = true + if (meta.persistentDataContainer.has(sectorKey, PersistentDataType.INTEGER)) { + sectorId = meta.persistentDataContainer.get(sectorKey, PersistentDataType.INTEGER) + break // Found linked tool in inventory. + } + } + } + } } if (!hasTool) return + // 1. Visualize Existing Ranges (Green) + if (sectorId != null) { + // World Check + val sector = sectorService.getSector(sectorId) + if (sector != null && sector.world == player.world.name) { + val shapes = sectorService.getSectorShapes(sectorId) + val color = Color.fromRGB(0, 255, 0) // Green for saved + + for (shape in shapes) { + when (shape) { + is net.hareworks.hcu.lands.model.Shape.Cuboid -> { + val minX = minOf(shape.x1, shape.x2).toDouble() + val minY = minOf(shape.y1, shape.y2).toDouble() + val minZ = minOf(shape.z1, shape.z2).toDouble() + val maxX = maxOf(shape.x1, shape.x2).toDouble() + 1.0 + val maxY = maxOf(shape.y1, shape.y2).toDouble() + 1.0 + val maxZ = maxOf(shape.z1, shape.z2).toDouble() + 1.0 + + GeometryVisualizer.drawCuboid( + player, + minX, minY, minZ, + maxX, maxY, maxZ, + color + ) + } + is net.hareworks.hcu.lands.model.Shape.Cylinder -> { + val minY = (shape.y - shape.bottomHeight).toDouble() + val maxY = (shape.y + shape.topHeight + 1).toDouble() + + GeometryVisualizer.drawCylinder( + player, + shape.x + 0.5, shape.y.toDouble(), shape.z + 0.5, + shape.radius.toInt(), + minY, + maxY, + color + ) + } + } + } + } + } + + // 2. Visualize Current Selection (Blue) 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 (p2 == null) return if (p1.world != p2.world) return - + + // ... (Existing Draw Logic for Selection) ... + // ... (Existing Draw Logic for Selection) ... if (selection.mode == SelectionMode.CUBOID) { - // Cuboid visualization var minX: Double var minY: Double var minZ: Double @@ -70,8 +138,8 @@ class SelectionVisualizerTask( var maxY: Double var maxZ: Double - if (!selection.p1Sneaking) { - // Normal: corner to corner + if (!selection.isCenterMode) { + // Normal: Corner to Corner (Diagonal) minX = minOf(p1.blockX, p2.blockX).toDouble() minY = minOf(p1.blockY, p2.blockY).toDouble() minZ = minOf(p1.blockZ, p2.blockZ).toDouble() @@ -79,7 +147,7 @@ class SelectionVisualizerTask( 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 + // Center Mode: P1 is Center val dx = abs(p1.blockX - p2.blockX) val dy = abs(p1.blockY - p2.blockY) val dz = abs(p1.blockZ - p2.blockZ) @@ -95,9 +163,8 @@ class SelectionVisualizerTask( GeometryVisualizer.drawCuboid(player, minX, minY, minZ, maxX, maxY, maxZ) } else { - // Cylinder visualization val centerX: Double - val centerY: Double + val centerY: Double // This variable is actually unused in the logic below if we override args, but kept for clarity if needed val centerZ: Double val radius: Double val minY: Double @@ -108,88 +175,55 @@ class SelectionVisualizerTask( 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 + if (!selection.isCenterMode) { + // Normal: P1 is Center, Symmetric Height centerX = p1.blockX + 0.5 - centerY = p1.blockY + 0.5 // Logic center + centerY = p1.blockY + 0.5 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. + // Center/Base Mode: P1 is Base, P2 defines Height/Radius 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>() - // 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. + // Draw Cylinder + GeometryVisualizer.drawCylinder(player, centerX, p1.blockY.toDouble(), centerZ, radius.toInt(), minY, maxY) + + // Draw Outlines (Surface) + val cX = kotlin.math.floor(centerX).toInt() + val cZ = kotlin.math.floor(centerZ).toInt() + val blocks = mutableSetOf>() + val actualRadius = radius + 0.5 + val radiusSq = actualRadius * actualRadius + val rInt = actualRadius.toInt() + 1 + + for (dx in -rInt..rInt) { + for (dz in -rInt..rInt) { + if (dx * dx + dz * dz <= radiusSq) { + blocks.add(Pair(cX + dx, cZ + dz)) + } + } + } + + val color = Color.fromRGB(100, 200, 255) + GeometryVisualizer.drawBlockSurfaceOutline(player, minY, blocks, { _, _, _ -> false }, color, minY) + GeometryVisualizer.drawBlockSurfaceOutline(player, maxY, blocks, { _, _, _ -> false }, color, maxY) } - } } +// Private helper extensions if needed for Shape +private fun net.hareworks.hcu.lands.model.Shape.worldName(): String { return "" } // Dummy, not used because we can't easily check +