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) server.pluginManager.registerEvents(net.hareworks.hcu.landsector.listener.SelectionListener(this, selService, service), this)
// Schedule visualization task // 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 { } else {
logger.severe("SelectionService not initialized!") logger.severe("SelectionService not initialized!")
} }

View File

@ -303,13 +303,21 @@ class SectorListener(
} else { } else {
content.append(Component.text("Parts:", NamedTextColor.BLACK)) content.append(Component.text("Parts:", NamedTextColor.BLACK))
ranges.forEach { range -> ranges.forEach { range ->
content.append(Component.text("\n- [${range.id}] ${range.type}", NamedTextColor.DARK_GRAY)) content.append(Component.text("\n"))
content.append(Component.text(" "))
content.append( content.append(
Component.text("[x] ", NamedTextColor.RED) Component.text("[x] ", NamedTextColor.RED)
.clickEvent(ClickEvent.runCommand("/landsector range delete $sectorId ${range.id}")) .clickEvent(ClickEvent.runCommand("/landsector range delete $sectorId ${range.id}"))
.hoverEvent(net.kyori.adventure.text.event.HoverEvent.showText(Component.text("Click to delete range"))) .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 player = event.player
val selection = selectionService.getSelection(player.uniqueId) 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) { if (event.action == Action.LEFT_CLICK_AIR || event.action == Action.LEFT_CLICK_BLOCK) {
event.isCancelled = true // Prevent breaking block event.isCancelled = true
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 return
} }
// Right Click: Set Points // Right Click: Set Points
if (event.action == Action.RIGHT_CLICK_BLOCK) { if (event.action == Action.RIGHT_CLICK_BLOCK || event.action == Action.RIGHT_CLICK_AIR) {
event.isCancelled = true // Prevent placing event.isCancelled = true // Prevent placing or item usage
val clickedBlock = event.clickedBlock ?: return val targetBlock = event.clickedBlock ?: player.rayTraceBlocks(100.0)?.hitBlock
val loc = clickedBlock.location
if (targetBlock == null) return
val loc = targetBlock.location
if (selection.point1 == null) { if (selection.point1 == null) {
// Set Point 1 // Set Point 1
selection.point1 = loc selection.point1 = loc
selection.p1Sneaking = player.isSneaking // selection.isCenterMode is persistent, so we don't set it here based on sneaking
selection.point2 = null // Clear p2 just in case 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 { } else {
// Set Point 2 // Set Point 2
// If P2 is already set, reset to P1 new? standard standard is cyclic.
if (selection.point2 != null) { if (selection.point2 != null) {
// Resetting, treat as P1 // Resetting, treat as P1
selection.point1 = loc selection.point1 = loc
selection.p1Sneaking = player.isSneaking
selection.point2 = null 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 { } else {
selection.point2 = loc selection.point2 = loc
// Check for sector ID // Check for sector ID
@ -90,10 +85,11 @@ class SelectionListener(
selection.mode, selection.mode,
p1.blockX, p1.blockY, p1.blockZ, p1.blockX, p1.blockY, p1.blockZ,
p2.blockX, p2.blockY, p2.blockZ, p2.blockX, p2.blockY, p2.blockZ,
selection.p1Sneaking selection.isCenterMode
) )
player.sendMessage(Component.text("Range added to Sector #$sId.", NamedTextColor.GREEN)) 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 // Clear selection
selection.point1 = null selection.point1 = null
@ -104,6 +100,8 @@ class SelectionListener(
// Show Cost / Area // Show Cost / Area
player.sendMessage(Component.text(selectionService.getAreaDetails(player.uniqueId), NamedTextColor.AQUA)) player.sendMessage(Component.text(selectionService.getAreaDetails(player.uniqueId), NamedTextColor.AQUA))
player.sendMessage(Component.text("Cost: ${selectionService.getCost(player.uniqueId)}", NamedTextColor.GOLD)) 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") val key = NamespacedKey(plugin, "component")
if (item.itemMeta?.persistentDataContainer?.get(key, PersistentDataType.STRING) == "land_sector_tool") { if (item.itemMeta?.persistentDataContainer?.get(key, PersistentDataType.STRING) == "land_sector_tool") {
event.isCancelled = true 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 mode: SelectionMode = SelectionMode.CUBOID,
var point1: Location? = null, var point1: Location? = null,
var point2: 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.SectorDraftsTable
import net.hareworks.hcu.landsector.database.SectorsTable import net.hareworks.hcu.landsector.database.SectorsTable
// SectorRangesTable removed
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.SectorRange
import net.hareworks.hcu.landsector.model.SelectionMode 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.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
@ -385,7 +386,7 @@ class SectorService(private val database: Database, private val landService: net
sectorId, sectorId,
SelectionMode.CYLINDER, SelectionMode.CYLINDER,
shape.x, shape.y, shape.z, shape.x, shape.y, shape.z,
shape.x, shape.y, shape.z, shape.radius.toInt(), shape.bottomHeight, shape.topHeight,
false 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 { fun deleteRange(sectorId: Int, rangeIndex: Int): Boolean {
val sector = getSector(sectorId) ?: return false val sector = getSector(sectorId) ?: return false
if (sector.landId != null) return false // Cannot edit confirmed 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 height: Int
val length: Int val length: Int
if (!data.p1Sneaking) { if (!data.isCenterMode) {
// Normal: P1 to P2 // Normal: P1 to P2
width = abs(p1.blockX - p2.blockX) + 1 width = abs(p1.blockX - p2.blockX) + 1
height = abs(p1.blockY - p2.blockY) + 1 height = abs(p1.blockY - p2.blockY) + 1
length = abs(p1.blockZ - p2.blockZ) + 1 length = abs(p1.blockZ - p2.blockZ) + 1
} else { } else {
// Sneak: P1 is center // Center Mode: P1 is center
val dx = abs(p1.blockX - p2.blockX) val dx = abs(p1.blockX - p2.blockX)
val dy = abs(p1.blockY - p2.blockY) val dy = abs(p1.blockY - p2.blockY)
val dz = abs(p1.blockZ - p2.blockZ) val dz = abs(p1.blockZ - p2.blockZ)
@ -58,12 +58,12 @@ class SelectionService {
val baseRadius = ceil(dist - 0.5) val baseRadius = ceil(dist - 0.5)
val radius = baseRadius + 0.5 val radius = baseRadius + 0.5
val totalHeight = if (!data.p1Sneaking) { val totalHeight = if (!data.isCenterMode) {
// Normal: P1 center, symmetric height // Normal: P1 center, symmetric height
val h = abs(p1.blockY - p2.blockY) val h = abs(p1.blockY - p2.blockY)
h * 2 + 1 h * 2 + 1
} else { } else {
// Sneak: P1 base, P2 top // Center Mode: P1 base, P2 top (or vice versa, height is diff)
abs(p1.blockY - p2.blockY) + 1 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.model.SelectionMode
import net.hareworks.hcu.landsector.service.SelectionService import net.hareworks.hcu.landsector.service.SelectionService
import net.hareworks.hcu.landsector.service.SectorService
import net.hareworks.hcu.visualizer.GeometryVisualizer import net.hareworks.hcu.visualizer.GeometryVisualizer
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
@ -18,7 +19,8 @@ import kotlin.math.abs
class SelectionVisualizerTask( class SelectionVisualizerTask(
private val plugin: JavaPlugin, private val plugin: JavaPlugin,
private val selectionService: SelectionService private val selectionService: SelectionService,
private val sectorService: SectorService
) : BukkitRunnable() { ) : BukkitRunnable() {
override fun run() { override fun run() {
@ -28,41 +30,107 @@ class SelectionVisualizerTask(
} }
private fun visualize(player: Player) { private fun visualize(player: Player) {
// Check if holding tool
// Check if tool is in inventory
val key = NamespacedKey(plugin, "component") val key = NamespacedKey(plugin, "component")
val hasTool = player.inventory.contents.any { item -> val sectorKey = NamespacedKey(plugin, "sector_id")
item != null && item.type == Material.FLINT &&
item.itemMeta?.persistentDataContainer?.get(key, PersistentDataType.STRING) == "land_sector_tool" // 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 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 selection = selectionService.getSelection(player.uniqueId)
val p1 = selection.point1 ?: return 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 p2 = selection.point2
var isDynamic = false
if (p2 == null) { if (p2 == null) {
val target = player.getTargetBlockExact(30) val target = player.getTargetBlockExact(30)
if (target != null) { if (target != null) {
p2 = target.location p2 = target.location
isDynamic = true
} }
} }
if (p2 == null) return // No p2 and no target if (p2 == null) return
// If different world, ignore
if (p1.world != p2.world) return if (p1.world != p2.world) return
// ... (Existing Draw Logic for Selection) ...
// ... (Existing Draw Logic for Selection) ...
if (selection.mode == SelectionMode.CUBOID) { if (selection.mode == SelectionMode.CUBOID) {
// Cuboid visualization
var minX: Double var minX: Double
var minY: Double var minY: Double
var minZ: Double var minZ: Double
@ -70,8 +138,8 @@ class SelectionVisualizerTask(
var maxY: Double var maxY: Double
var maxZ: Double var maxZ: Double
if (!selection.p1Sneaking) { if (!selection.isCenterMode) {
// Normal: corner to corner // Normal: Corner to Corner (Diagonal)
minX = minOf(p1.blockX, p2.blockX).toDouble() minX = minOf(p1.blockX, p2.blockX).toDouble()
minY = minOf(p1.blockY, p2.blockY).toDouble() minY = minOf(p1.blockY, p2.blockY).toDouble()
minZ = minOf(p1.blockZ, p2.blockZ).toDouble() minZ = minOf(p1.blockZ, p2.blockZ).toDouble()
@ -79,7 +147,7 @@ class SelectionVisualizerTask(
maxY = maxOf(p1.blockY, p2.blockY).toDouble() + 1.0 maxY = maxOf(p1.blockY, p2.blockY).toDouble() + 1.0
maxZ = maxOf(p1.blockZ, p2.blockZ).toDouble() + 1.0 maxZ = maxOf(p1.blockZ, p2.blockZ).toDouble() + 1.0
} else { } else {
// Sneak: P1 center, P2 defines radius // Center Mode: P1 is Center
val dx = abs(p1.blockX - p2.blockX) val dx = abs(p1.blockX - p2.blockX)
val dy = abs(p1.blockY - p2.blockY) val dy = abs(p1.blockY - p2.blockY)
val dz = abs(p1.blockZ - p2.blockZ) val dz = abs(p1.blockZ - p2.blockZ)
@ -95,9 +163,8 @@ class SelectionVisualizerTask(
GeometryVisualizer.drawCuboid(player, minX, minY, minZ, maxX, maxY, maxZ) GeometryVisualizer.drawCuboid(player, minX, minY, minZ, maxX, maxY, maxZ)
} else { } else {
// Cylinder visualization
val centerX: Double 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 centerZ: Double
val radius: Double val radius: Double
val minY: Double val minY: Double
@ -108,88 +175,55 @@ class SelectionVisualizerTask(
val dist = Math.sqrt((dx * dx + dz * dz).toDouble()) val dist = Math.sqrt((dx * dx + dz * dz).toDouble())
radius = kotlin.math.ceil(dist - 0.5) radius = kotlin.math.ceil(dist - 0.5)
if (!selection.p1Sneaking) { if (!selection.isCenterMode) {
// Normal: P1 center, symmetric height based on P2.y diff // Normal: P1 is Center, Symmetric Height
centerX = p1.blockX + 0.5 centerX = p1.blockX + 0.5
centerY = p1.blockY + 0.5 // Logic center centerY = p1.blockY + 0.5
centerZ = p1.blockZ + 0.5 centerZ = p1.blockZ + 0.5
val hDiff = abs(p1.blockY - p2.blockY) 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 baseBlockY = p1.blockY - hDiff
val topBlockY = p1.blockY + hDiff val topBlockY = p1.blockY + hDiff
minY = baseBlockY.toDouble() minY = baseBlockY.toDouble()
maxY = topBlockY.toDouble() + 1.0 maxY = topBlockY.toDouble() + 1.0
} else { } else {
// Sneak: P1 base center. P2 top center. // Center/Base Mode: P1 is Base, P2 defines Height/Radius
centerX = p1.blockX + 0.5 centerX = p1.blockX + 0.5
centerZ = p1.blockZ + 0.5 centerZ = p1.blockZ + 0.5
// Only height from p2
val y1 = p1.blockY val y1 = p1.blockY
val y2 = p2.blockY val y2 = p2.blockY
val baseBlockY = minOf(y1, y2) val baseBlockY = minOf(y1, y2)
val topBlockY = maxOf(y1, y2) val topBlockY = maxOf(y1, y2)
minY = baseBlockY.toDouble() minY = baseBlockY.toDouble()
maxY = topBlockY.toDouble() + 1.0 maxY = topBlockY.toDouble() + 1.0
} }
// Calculate blocks for cylinder outline // 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 cX = kotlin.math.floor(centerX).toInt()
val cZ = kotlin.math.floor(centerZ).toInt() val cZ = kotlin.math.floor(centerZ).toInt()
val blocks = mutableSetOf<Pair<Int, Int>>() 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 actualRadius = radius + 0.5
val radiusSq = actualRadius * actualRadius val radiusSq = actualRadius * actualRadius
val rInt = actualRadius.toInt() + 1 val rInt = actualRadius.toInt() + 1
for (dx in -rInt..rInt) { for (dx in -rInt..rInt) {
for (dz in -rInt..rInt) { for (dz in -rInt..rInt) {
// Check if block center is within the radius
if (dx * dx + dz * dz <= radiusSq) { if (dx * dx + dz * dz <= radiusSq) {
blocks.add(Pair(cX + dx, cZ + dz)) 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) val color = Color.fromRGB(100, 200, 255)
// Draw bottom surface outline GeometryVisualizer.drawBlockSurfaceOutline(player, minY, blocks, { _, _, _ -> false }, color, minY)
GeometryVisualizer.drawBlockSurfaceOutline( GeometryVisualizer.drawBlockSurfaceOutline(player, maxY, blocks, { _, _, _ -> false }, color, maxY)
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.
}
} }
} }
}
// 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("com.michael-bull.kotlin-result:kotlin-result:2.1.0")
compileOnly("net.hareworks:kommand-lib:1.1") compileOnly("net.hareworks:kommand-lib:1.1")
compileOnly("net.hareworks:permits-lib") 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) // Visualizer Lib (Bundled & Relocated)
implementation("net.hareworks.hcu:visualizer-lib:1.0") 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) server.pluginManager.registerEvents(net.hareworks.hcu.landsector.listener.SelectionListener(this, selService, service), this)
// Schedule visualization task // 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 { } else {
logger.severe("SelectionService not initialized!") logger.severe("SelectionService not initialized!")
} }

View File

@ -303,13 +303,21 @@ class SectorListener(
} else { } else {
content.append(Component.text("Parts:", NamedTextColor.BLACK)) content.append(Component.text("Parts:", NamedTextColor.BLACK))
ranges.forEach { range -> ranges.forEach { range ->
content.append(Component.text("\n- [${range.id}] ${range.type}", NamedTextColor.DARK_GRAY)) content.append(Component.text("\n"))
content.append(Component.text(" "))
content.append( content.append(
Component.text("[x] ", NamedTextColor.RED) Component.text("[x] ", NamedTextColor.RED)
.clickEvent(ClickEvent.runCommand("/landsector range delete $sectorId ${range.id}")) .clickEvent(ClickEvent.runCommand("/landsector range delete $sectorId ${range.id}"))
.hoverEvent(net.kyori.adventure.text.event.HoverEvent.showText(Component.text("Click to delete range"))) .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 player = event.player
val selection = selectionService.getSelection(player.uniqueId) 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) { if (event.action == Action.LEFT_CLICK_AIR || event.action == Action.LEFT_CLICK_BLOCK) {
event.isCancelled = true // Prevent breaking block event.isCancelled = true
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 return
} }
// Right Click: Set Points // Right Click: Set Points
if (event.action == Action.RIGHT_CLICK_BLOCK) { if (event.action == Action.RIGHT_CLICK_BLOCK || event.action == Action.RIGHT_CLICK_AIR) {
event.isCancelled = true // Prevent placing event.isCancelled = true // Prevent placing or item usage
val clickedBlock = event.clickedBlock ?: return val targetBlock = event.clickedBlock ?: player.rayTraceBlocks(100.0)?.hitBlock
val loc = clickedBlock.location
if (targetBlock == null) return
val loc = targetBlock.location
if (selection.point1 == null) { if (selection.point1 == null) {
// Set Point 1 // Set Point 1
selection.point1 = loc selection.point1 = loc
selection.p1Sneaking = player.isSneaking // selection.isCenterMode is persistent, so we don't set it here based on sneaking
selection.point2 = null // Clear p2 just in case 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 { } else {
// Set Point 2 // Set Point 2
// If P2 is already set, reset to P1 new? standard standard is cyclic.
if (selection.point2 != null) { if (selection.point2 != null) {
// Resetting, treat as P1 // Resetting, treat as P1
selection.point1 = loc selection.point1 = loc
selection.p1Sneaking = player.isSneaking
selection.point2 = null 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 { } else {
selection.point2 = loc selection.point2 = loc
// Check for sector ID // Check for sector ID
@ -90,10 +85,11 @@ class SelectionListener(
selection.mode, selection.mode,
p1.blockX, p1.blockY, p1.blockZ, p1.blockX, p1.blockY, p1.blockZ,
p2.blockX, p2.blockY, p2.blockZ, p2.blockX, p2.blockY, p2.blockZ,
selection.p1Sneaking selection.isCenterMode
) )
player.sendMessage(Component.text("Range added to Sector #$sId.", NamedTextColor.GREEN)) 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 // Clear selection
selection.point1 = null selection.point1 = null
@ -104,6 +100,8 @@ class SelectionListener(
// Show Cost / Area // Show Cost / Area
player.sendMessage(Component.text(selectionService.getAreaDetails(player.uniqueId), NamedTextColor.AQUA)) player.sendMessage(Component.text(selectionService.getAreaDetails(player.uniqueId), NamedTextColor.AQUA))
player.sendMessage(Component.text("Cost: ${selectionService.getCost(player.uniqueId)}", NamedTextColor.GOLD)) 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") val key = NamespacedKey(plugin, "component")
if (item.itemMeta?.persistentDataContainer?.get(key, PersistentDataType.STRING) == "land_sector_tool") { if (item.itemMeta?.persistentDataContainer?.get(key, PersistentDataType.STRING) == "land_sector_tool") {
event.isCancelled = true 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 mode: SelectionMode = SelectionMode.CUBOID,
var point1: Location? = null, var point1: Location? = null,
var point2: 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.SectorDraftsTable
import net.hareworks.hcu.landsector.database.SectorsTable import net.hareworks.hcu.landsector.database.SectorsTable
// SectorRangesTable removed
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.SectorRange
import net.hareworks.hcu.landsector.model.SelectionMode 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.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
@ -385,7 +386,7 @@ class SectorService(private val database: Database, private val landService: net
sectorId, sectorId,
SelectionMode.CYLINDER, SelectionMode.CYLINDER,
shape.x, shape.y, shape.z, shape.x, shape.y, shape.z,
shape.x, shape.y, shape.z, shape.radius.toInt(), shape.bottomHeight, shape.topHeight,
false 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 { fun deleteRange(sectorId: Int, rangeIndex: Int): Boolean {
val sector = getSector(sectorId) ?: return false val sector = getSector(sectorId) ?: return false
if (sector.landId != null) return false // Cannot edit confirmed 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 height: Int
val length: Int val length: Int
if (!data.p1Sneaking) { if (!data.isCenterMode) {
// Normal: P1 to P2 // Normal: P1 to P2
width = abs(p1.blockX - p2.blockX) + 1 width = abs(p1.blockX - p2.blockX) + 1
height = abs(p1.blockY - p2.blockY) + 1 height = abs(p1.blockY - p2.blockY) + 1
length = abs(p1.blockZ - p2.blockZ) + 1 length = abs(p1.blockZ - p2.blockZ) + 1
} else { } else {
// Sneak: P1 is center // Center Mode: P1 is center
val dx = abs(p1.blockX - p2.blockX) val dx = abs(p1.blockX - p2.blockX)
val dy = abs(p1.blockY - p2.blockY) val dy = abs(p1.blockY - p2.blockY)
val dz = abs(p1.blockZ - p2.blockZ) val dz = abs(p1.blockZ - p2.blockZ)
@ -58,12 +58,12 @@ class SelectionService {
val baseRadius = ceil(dist - 0.5) val baseRadius = ceil(dist - 0.5)
val radius = baseRadius + 0.5 val radius = baseRadius + 0.5
val totalHeight = if (!data.p1Sneaking) { val totalHeight = if (!data.isCenterMode) {
// Normal: P1 center, symmetric height // Normal: P1 center, symmetric height
val h = abs(p1.blockY - p2.blockY) val h = abs(p1.blockY - p2.blockY)
h * 2 + 1 h * 2 + 1
} else { } else {
// Sneak: P1 base, P2 top // Center Mode: P1 base, P2 top (or vice versa, height is diff)
abs(p1.blockY - p2.blockY) + 1 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.model.SelectionMode
import net.hareworks.hcu.landsector.service.SelectionService import net.hareworks.hcu.landsector.service.SelectionService
import net.hareworks.hcu.landsector.service.SectorService
import net.hareworks.hcu.visualizer.GeometryVisualizer import net.hareworks.hcu.visualizer.GeometryVisualizer
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
@ -18,7 +19,8 @@ import kotlin.math.abs
class SelectionVisualizerTask( class SelectionVisualizerTask(
private val plugin: JavaPlugin, private val plugin: JavaPlugin,
private val selectionService: SelectionService private val selectionService: SelectionService,
private val sectorService: SectorService
) : BukkitRunnable() { ) : BukkitRunnable() {
override fun run() { override fun run() {
@ -28,41 +30,107 @@ class SelectionVisualizerTask(
} }
private fun visualize(player: Player) { private fun visualize(player: Player) {
// Check if holding tool
// Check if tool is in inventory
val key = NamespacedKey(plugin, "component") val key = NamespacedKey(plugin, "component")
val hasTool = player.inventory.contents.any { item -> val sectorKey = NamespacedKey(plugin, "sector_id")
item != null && item.type == Material.FLINT &&
item.itemMeta?.persistentDataContainer?.get(key, PersistentDataType.STRING) == "land_sector_tool" // 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 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 selection = selectionService.getSelection(player.uniqueId)
val p1 = selection.point1 ?: return 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 p2 = selection.point2
var isDynamic = false
if (p2 == null) { if (p2 == null) {
val target = player.getTargetBlockExact(30) val target = player.getTargetBlockExact(30)
if (target != null) { if (target != null) {
p2 = target.location p2 = target.location
isDynamic = true
} }
} }
if (p2 == null) return // No p2 and no target if (p2 == null) return
// If different world, ignore
if (p1.world != p2.world) return if (p1.world != p2.world) return
// ... (Existing Draw Logic for Selection) ...
// ... (Existing Draw Logic for Selection) ...
if (selection.mode == SelectionMode.CUBOID) { if (selection.mode == SelectionMode.CUBOID) {
// Cuboid visualization
var minX: Double var minX: Double
var minY: Double var minY: Double
var minZ: Double var minZ: Double
@ -70,8 +138,8 @@ class SelectionVisualizerTask(
var maxY: Double var maxY: Double
var maxZ: Double var maxZ: Double
if (!selection.p1Sneaking) { if (!selection.isCenterMode) {
// Normal: corner to corner // Normal: Corner to Corner (Diagonal)
minX = minOf(p1.blockX, p2.blockX).toDouble() minX = minOf(p1.blockX, p2.blockX).toDouble()
minY = minOf(p1.blockY, p2.blockY).toDouble() minY = minOf(p1.blockY, p2.blockY).toDouble()
minZ = minOf(p1.blockZ, p2.blockZ).toDouble() minZ = minOf(p1.blockZ, p2.blockZ).toDouble()
@ -79,7 +147,7 @@ class SelectionVisualizerTask(
maxY = maxOf(p1.blockY, p2.blockY).toDouble() + 1.0 maxY = maxOf(p1.blockY, p2.blockY).toDouble() + 1.0
maxZ = maxOf(p1.blockZ, p2.blockZ).toDouble() + 1.0 maxZ = maxOf(p1.blockZ, p2.blockZ).toDouble() + 1.0
} else { } else {
// Sneak: P1 center, P2 defines radius // Center Mode: P1 is Center
val dx = abs(p1.blockX - p2.blockX) val dx = abs(p1.blockX - p2.blockX)
val dy = abs(p1.blockY - p2.blockY) val dy = abs(p1.blockY - p2.blockY)
val dz = abs(p1.blockZ - p2.blockZ) val dz = abs(p1.blockZ - p2.blockZ)
@ -95,9 +163,8 @@ class SelectionVisualizerTask(
GeometryVisualizer.drawCuboid(player, minX, minY, minZ, maxX, maxY, maxZ) GeometryVisualizer.drawCuboid(player, minX, minY, minZ, maxX, maxY, maxZ)
} else { } else {
// Cylinder visualization
val centerX: Double 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 centerZ: Double
val radius: Double val radius: Double
val minY: Double val minY: Double
@ -108,88 +175,55 @@ class SelectionVisualizerTask(
val dist = Math.sqrt((dx * dx + dz * dz).toDouble()) val dist = Math.sqrt((dx * dx + dz * dz).toDouble())
radius = kotlin.math.ceil(dist - 0.5) radius = kotlin.math.ceil(dist - 0.5)
if (!selection.p1Sneaking) { if (!selection.isCenterMode) {
// Normal: P1 center, symmetric height based on P2.y diff // Normal: P1 is Center, Symmetric Height
centerX = p1.blockX + 0.5 centerX = p1.blockX + 0.5
centerY = p1.blockY + 0.5 // Logic center centerY = p1.blockY + 0.5
centerZ = p1.blockZ + 0.5 centerZ = p1.blockZ + 0.5
val hDiff = abs(p1.blockY - p2.blockY) 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 baseBlockY = p1.blockY - hDiff
val topBlockY = p1.blockY + hDiff val topBlockY = p1.blockY + hDiff
minY = baseBlockY.toDouble() minY = baseBlockY.toDouble()
maxY = topBlockY.toDouble() + 1.0 maxY = topBlockY.toDouble() + 1.0
} else { } else {
// Sneak: P1 base center. P2 top center. // Center/Base Mode: P1 is Base, P2 defines Height/Radius
centerX = p1.blockX + 0.5 centerX = p1.blockX + 0.5
centerZ = p1.blockZ + 0.5 centerZ = p1.blockZ + 0.5
// Only height from p2
val y1 = p1.blockY val y1 = p1.blockY
val y2 = p2.blockY val y2 = p2.blockY
val baseBlockY = minOf(y1, y2) val baseBlockY = minOf(y1, y2)
val topBlockY = maxOf(y1, y2) val topBlockY = maxOf(y1, y2)
minY = baseBlockY.toDouble() minY = baseBlockY.toDouble()
maxY = topBlockY.toDouble() + 1.0 maxY = topBlockY.toDouble() + 1.0
} }
// Calculate blocks for cylinder outline // 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 cX = kotlin.math.floor(centerX).toInt()
val cZ = kotlin.math.floor(centerZ).toInt() val cZ = kotlin.math.floor(centerZ).toInt()
val blocks = mutableSetOf<Pair<Int, Int>>() 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 actualRadius = radius + 0.5
val radiusSq = actualRadius * actualRadius val radiusSq = actualRadius * actualRadius
val rInt = actualRadius.toInt() + 1 val rInt = actualRadius.toInt() + 1
for (dx in -rInt..rInt) { for (dx in -rInt..rInt) {
for (dz in -rInt..rInt) { for (dz in -rInt..rInt) {
// Check if block center is within the radius
if (dx * dx + dz * dz <= radiusSq) { if (dx * dx + dz * dz <= radiusSq) {
blocks.add(Pair(cX + dx, cZ + dz)) 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) val color = Color.fromRGB(100, 200, 255)
// Draw bottom surface outline GeometryVisualizer.drawBlockSurfaceOutline(player, minY, blocks, { _, _, _ -> false }, color, minY)
GeometryVisualizer.drawBlockSurfaceOutline( GeometryVisualizer.drawBlockSurfaceOutline(player, maxY, blocks, { _, _, _ -> false }, color, maxY)
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.
}
} }
} }
}
// 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