feat:範囲設定・可視化

This commit is contained in:
Keisuke Hirata 2025-12-20 05:55:15 +09:00
parent 767f974228
commit 3c6b644607
16 changed files with 382 additions and 238 deletions

2
Lands

@ -1 +1 @@
Subproject commit 0f432b0e89614bc07bd710423284da5e0228e25a
Subproject commit d256db9f518bb2a6444a36f71b69afee783fd077

View File

@ -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!")
}

View File

@ -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))
}
}

View File

@ -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
}
}
}

View File

@ -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
)

View File

@ -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<net.hareworks.hcu.lands.model.Shape> {
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
}
}
}

View File

@ -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
}

View File

@ -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<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
// Draw Cylinder
GeometryVisualizer.drawCylinder(player, centerX, p1.blockY.toDouble(), centerZ, radius.toInt(), minY, maxY)
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 Outlines (Surface)
val cX = kotlin.math.floor(centerX).toInt()
val cZ = kotlin.math.floor(centerZ).toInt()
val blocks = mutableSetOf<Pair<Int, Int>>()
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))
}
}
}
// 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.
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

View File

@ -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")

View File

@ -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!")
}

View File

@ -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))
}
}

View File

@ -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
}
}
}

View File

@ -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
)

View File

@ -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<net.hareworks.hcu.lands.model.Shape> {
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
}
}
}

View File

@ -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
}

View File

@ -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<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
// Draw Cylinder
GeometryVisualizer.drawCylinder(player, centerX, p1.blockY.toDouble(), centerZ, radius.toInt(), minY, maxY)
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 Outlines (Surface)
val cX = kotlin.math.floor(centerX).toInt()
val cZ = kotlin.math.floor(centerZ).toInt()
val blocks = mutableSetOf<Pair<Int, Int>>()
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))
}
}
}
// 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.
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