Compare commits

..

No commits in common. "0ed06e789f114b13ea3f2d78d7773858ede1fd80" and "c33cd859fee78c98035d88b73a6c37614b495f3c" have entirely different histories.

7 changed files with 65 additions and 644 deletions

@ -1 +0,0 @@
Subproject commit 520a378cd5c3da6321305d07cc0b402b73292add

View File

@ -55,20 +55,10 @@ class App : JavaPlugin() {
} }
this.playerIdService = pIdService this.playerIdService = pIdService
// Register public API as a Bukkit service
val landsAPI = net.hareworks.hcu.lands.api.LandsAPIImpl(service)
server.servicesManager.register(
net.hareworks.hcu.lands.api.LandsAPI::class.java,
landsAPI,
this,
org.bukkit.plugin.ServicePriority.Normal
)
// Run visualizer every 20 ticks (1 second) // Run visualizer every 20 ticks (1 second)
server.scheduler.runTaskTimer(this, VisualizerTask(service), 20L, 10L) server.scheduler.runTaskTimer(this, VisualizerTask(service), 20L, 10L)
logger.info("Lands plugin initialized successfully.") logger.info("Lands plugin initialized successfully.")
logger.info("LandsAPI registered as a Bukkit service.")
} }
override fun onDisable() { override fun onDisable() {

View File

@ -1,104 +0,0 @@
package net.hareworks.hcu.lands.api
import net.hareworks.hcu.lands.model.Land
import net.hareworks.hcu.lands.model.LandData
import org.bukkit.Location
import org.bukkit.block.Block
/**
* Public API for the Lands plugin.
* This service can be obtained via Bukkit's ServicesManager.
*
* Example usage:
* ```kotlin
* val landsAPI = Bukkit.getServicesManager().load(LandsAPI::class.java)
* val land = landsAPI?.getLandAt(block)
* ```
*/
interface LandsAPI {
/**
* Gets the land that contains the specified block.
*
* @param block The block to check
* @return The land containing the block, or null if no land contains it
*/
fun getLandAt(block: Block): Land?
/**
* Gets the land that contains the specified location.
*
* @param location The location to check
* @return The land containing the location, or null if no land contains it
*/
fun getLandAt(location: Location): Land?
/**
* Gets the land that contains the specified coordinates.
*
* @param world The world name
* @param x The x coordinate
* @param y The y coordinate
* @param z The z coordinate
* @return The land containing the coordinates, or null if no land contains it
*/
fun getLandAt(world: String, x: Double, y: Double, z: Double): Land?
/**
* Gets a land by its name.
*
* @param name The name of the land
* @return The land with the specified name, or null if not found
*/
fun getLand(name: String): Land?
/**
* Gets all lands in the specified world.
*
* @param world The world name
* @return List of all lands in the world
*/
fun getLandsInWorld(world: String): List<Land>
/**
* Gets all lands owned by the specified actor.
*
* @param actorId The actor ID
* @return List of all lands owned by the actor
*/
fun getLandsByOwner(actorId: Int): List<Land>
/**
* Gets all lands.
*
* @return List of all lands
*/
fun getAllLands(): List<Land>
/**
* Creates a new land.
*
* @param name The name of the land
* @param actorId The owner's actor ID
* @param world The world name
* @return true if the land was created successfully, false if a land with that name already exists
*/
fun createLand(name: String, actorId: Int, world: String): Boolean
/**
* Deletes a land.
*
* @param name The name of the land to delete
* @return true if the land was deleted successfully, false if the land doesn't exist
*/
fun deleteLand(name: String): Boolean
/**
* Modifies a land's data.
*
* @param name The name of the land to modify
* @param modifier A function that modifies the land's data
* @return true if the land was modified successfully, false if the land doesn't exist
*/
fun modifyLand(name: String, modifier: (LandData) -> Unit): Boolean
}

View File

@ -1,63 +0,0 @@
package net.hareworks.hcu.lands.api
import net.hareworks.hcu.lands.model.Land
import net.hareworks.hcu.lands.model.LandData
import net.hareworks.hcu.lands.service.LandService
import org.bukkit.Location
import org.bukkit.block.Block
/**
* Implementation of the LandsAPI interface.
* This is registered as a Bukkit service and delegates to the internal LandService.
*/
class LandsAPIImpl(private val landService: LandService) : LandsAPI {
override fun getLandAt(block: Block): Land? {
return getLandAt(block.world.name, block.x + 0.5, block.y + 0.5, block.z + 0.5)
}
override fun getLandAt(location: Location): Land? {
return getLandAt(location.world.name, location.x, location.y, location.z)
}
override fun getLandAt(world: String, x: Double, y: Double, z: Double): Land? {
return landService.getAllLands()
.filter { it.world == world }
.firstOrNull { land ->
land.data.parts.any { shape ->
when (shape) {
is net.hareworks.hcu.lands.model.Shape.Cuboid -> shape.contains(x, y, z)
is net.hareworks.hcu.lands.model.Shape.Cylinder -> shape.contains(x, y, z)
}
}
}
}
override fun getLand(name: String): Land? {
return landService.getLand(name)
}
override fun getLandsInWorld(world: String): List<Land> {
return landService.getAllLands().filter { it.world == world }.toList()
}
override fun getLandsByOwner(actorId: Int): List<Land> {
return landService.getAllLands().filter { it.actorId == actorId }.toList()
}
override fun getAllLands(): List<Land> {
return landService.getAllLands().toList()
}
override fun createLand(name: String, actorId: Int, world: String): Boolean {
return landService.createLand(name, actorId, world)
}
override fun deleteLand(name: String): Boolean {
return landService.deleteLand(name)
}
override fun modifyLand(name: String, modifier: (LandData) -> Unit): Boolean {
return landService.modifyLand(name, modifier)
}
}

View File

@ -4,7 +4,6 @@ import io.papermc.paper.math.Position
import net.hareworks.hcu.lands.App import net.hareworks.hcu.lands.App
import net.hareworks.hcu.lands.model.Land import net.hareworks.hcu.lands.model.Land
import net.hareworks.hcu.lands.model.Shape import net.hareworks.hcu.lands.model.Shape
import net.hareworks.hcu.lands.model.normalizeToBlockInt
import net.hareworks.hcu.lands.task.LandsVisualizer import net.hareworks.hcu.lands.task.LandsVisualizer
import net.hareworks.kommand_lib.kommand import net.hareworks.kommand_lib.kommand
import net.kyori.adventure.text.Component import net.kyori.adventure.text.Component
@ -89,14 +88,7 @@ class LandsCommand(
executes { executes {
val p1: Position = argument("pos1") val p1: Position = argument("pos1")
val p2: Position = argument("pos2") val p2: Position = argument("pos2")
val shape = Shape.Cuboid( val shape = Shape.Cuboid(p1.x(), p1.y(), p1.z(), p2.x(), p2.y(), p2.z())
normalizeToBlockInt(p1.x()),
normalizeToBlockInt(p1.y()),
normalizeToBlockInt(p1.z()),
normalizeToBlockInt(p2.x()),
normalizeToBlockInt(p2.y()),
normalizeToBlockInt(p2.z())
)
val name: String = argument("name") val name: String = argument("name")
updateLandData(sender, name) { parts -> updateLandData(sender, name) { parts ->
@ -110,26 +102,14 @@ class LandsCommand(
literal("cylinder") { literal("cylinder") {
blockCoordinates("center") { blockCoordinates("center") {
float("radius") { float("radius") {
literal("center") { float("height") {
integer("totalHeight", min = 1) {
executes { executes {
val c: Position = argument("center") val c: Position = argument("center")
val r: Double = argument("radius") val r: Double = argument("radius")
val total: Int = argument("totalHeight") val h: Double = argument("height")
val shape = Shape.Cylinder(c.x(), c.y(), c.z(), r, h)
val name: String = argument("name") val name: String = argument("name")
// Subtract 1 for center block, distribute remaining
val remaining = total - 1
val topH = (remaining + 1) / 2 // Prefer top
val bottomH = remaining - topH
val shape = Shape.Cylinder(
normalizeToBlockInt(c.x()),
normalizeToBlockInt(c.y()),
normalizeToBlockInt(c.z()),
r, bottomH, topH
)
updateLandData(sender, name) { parts -> updateLandData(sender, name) { parts ->
parts.add(shape) parts.add(shape)
sender.sendMessage(Component.text("Shape added.", NamedTextColor.GREEN)) sender.sendMessage(Component.text("Shape added.", NamedTextColor.GREEN))
@ -137,76 +117,6 @@ class LandsCommand(
} }
} }
} }
literal("top") {
integer("topHeight", min = 1) {
literal("bottom") {
integer("bottomHeight", min = 0) {
executes {
val c: Position = argument("center")
val r: Double = argument("radius")
val topH: Int = argument("topHeight")
val bottomH: Int = argument("bottomHeight")
val name: String = argument("name")
val shape = Shape.Cylinder(
normalizeToBlockInt(c.x()),
normalizeToBlockInt(c.y()),
normalizeToBlockInt(c.z()),
r, bottomH, topH
)
updateLandData(sender, name) { parts ->
parts.add(shape)
sender.sendMessage(Component.text("Shape added.", NamedTextColor.GREEN))
}
}
}
}
executes {
val c: Position = argument("center")
val r: Double = argument("radius")
val total: Int = argument("topHeight")
val name: String = argument("name")
// total - 1 goes to top, 0 to bottom
val shape = Shape.Cylinder(
normalizeToBlockInt(c.x()),
normalizeToBlockInt(c.y()),
normalizeToBlockInt(c.z()),
r, 0, total - 1
)
updateLandData(sender, name) { parts ->
parts.add(shape)
sender.sendMessage(Component.text("Shape added.", NamedTextColor.GREEN))
}
}
}
}
literal("bottom") {
integer("bottomHeight", min = 1) {
executes {
val c: Position = argument("center")
val r: Double = argument("radius")
val total: Int = argument("bottomHeight")
val name: String = argument("name")
// total - 1 goes to bottom, 0 to top
val shape = Shape.Cylinder(
normalizeToBlockInt(c.x()),
normalizeToBlockInt(c.y()),
normalizeToBlockInt(c.z()),
r, total - 1, 0
)
updateLandData(sender, name) { parts ->
parts.add(shape)
sender.sendMessage(Component.text("Shape added.", NamedTextColor.GREEN))
}
}
}
}
}
} }
} }
} }
@ -220,14 +130,7 @@ class LandsCommand(
val index: Int = argument("index") val index: Int = argument("index")
val p1: Position = argument("pos1") val p1: Position = argument("pos1")
val p2: Position = argument("pos2") val p2: Position = argument("pos2")
val shape = Shape.Cuboid( val shape = Shape.Cuboid(p1.x(), p1.y(), p1.z(), p2.x(), p2.y(), p2.z())
normalizeToBlockInt(p1.x()),
normalizeToBlockInt(p1.y()),
normalizeToBlockInt(p1.z()),
normalizeToBlockInt(p2.x()),
normalizeToBlockInt(p2.y()),
normalizeToBlockInt(p2.z())
)
val name: String = argument("name") val name: String = argument("name")
updateLandData(sender, name) { parts -> updateLandData(sender, name) { parts ->
@ -245,26 +148,15 @@ class LandsCommand(
literal("cylinder") { literal("cylinder") {
blockCoordinates("center") { blockCoordinates("center") {
float("radius") { float("radius") {
literal("center") { float("height") {
integer("totalHeight", min = 1) {
executes { executes {
val index: Int = argument("index") val index: Int = argument("index")
val c: Position = argument("center") val c: Position = argument("center")
val r: Double = argument("radius") val r: Double = argument("radius")
val total: Int = argument("totalHeight") val h: Double = argument("height")
val shape = Shape.Cylinder(c.x(), c.y(), c.z(), r, h)
val name: String = argument("name") val name: String = argument("name")
val remaining = total - 1
val topH = (remaining + 1) / 2
val bottomH = remaining - topH
val shape = Shape.Cylinder(
normalizeToBlockInt(c.x()),
normalizeToBlockInt(c.y()),
normalizeToBlockInt(c.z()),
r, bottomH, topH
)
updateLandData(sender, name) { parts -> updateLandData(sender, name) { parts ->
if (index in parts.indices) { if (index in parts.indices) {
parts[index] = shape parts[index] = shape
@ -276,89 +168,6 @@ class LandsCommand(
} }
} }
} }
literal("top") {
integer("topHeight", min = 1) {
literal("bottom") {
integer("bottomHeight", min = 0) {
executes {
val index: Int = argument("index")
val c: Position = argument("center")
val r: Double = argument("radius")
val topH: Int = argument("topHeight")
val bottomH: Int = argument("bottomHeight")
val name: String = argument("name")
val shape = Shape.Cylinder(
normalizeToBlockInt(c.x()),
normalizeToBlockInt(c.y()),
normalizeToBlockInt(c.z()),
r, bottomH, topH
)
updateLandData(sender, name) { parts ->
if (index in parts.indices) {
parts[index] = shape
sender.sendMessage(Component.text("Shape modified at index $index.", NamedTextColor.GREEN))
} else {
sender.sendMessage(Component.text("Index out of bounds.", NamedTextColor.RED))
}
}
}
}
}
executes {
val index: Int = argument("index")
val c: Position = argument("center")
val r: Double = argument("radius")
val total: Int = argument("topHeight")
val name: String = argument("name")
val shape = Shape.Cylinder(
normalizeToBlockInt(c.x()),
normalizeToBlockInt(c.y()),
normalizeToBlockInt(c.z()),
r, 0, total - 1
)
updateLandData(sender, name) { parts ->
if (index in parts.indices) {
parts[index] = shape
sender.sendMessage(Component.text("Shape modified at index $index.", NamedTextColor.GREEN))
} else {
sender.sendMessage(Component.text("Index out of bounds.", NamedTextColor.RED))
}
}
}
}
}
literal("bottom") {
integer("bottomHeight", min = 1) {
executes {
val index: Int = argument("index")
val c: Position = argument("center")
val r: Double = argument("radius")
val total: Int = argument("bottomHeight")
val name: String = argument("name")
val shape = Shape.Cylinder(
normalizeToBlockInt(c.x()),
normalizeToBlockInt(c.y()),
normalizeToBlockInt(c.z()),
r, total - 1, 0
)
updateLandData(sender, name) { parts ->
if (index in parts.indices) {
parts[index] = shape
sender.sendMessage(Component.text("Shape modified at index $index.", NamedTextColor.GREEN))
} else {
sender.sendMessage(Component.text("Index out of bounds.", NamedTextColor.RED))
}
}
}
}
}
}
} }
} }
} }

View File

@ -21,8 +21,8 @@ sealed class Shape {
@Serializable @Serializable
@SerialName("cuboid") @SerialName("cuboid")
data class Cuboid( data class Cuboid(
val x1: Int, val y1: Int, val z1: Int, val x1: Double, val y1: Double, val z1: Double,
val x2: Int, val y2: Int, val z2: Int val x2: Double, val y2: Double, val z2: Double
) : Shape() { ) : Shape() {
fun minX() = minOf(x1, x2) fun minX() = minOf(x1, x2)
fun maxX() = maxOf(x1, x2) fun maxX() = maxOf(x1, x2)
@ -32,116 +32,24 @@ sealed class Shape {
fun maxZ() = maxOf(z1, z2) fun maxZ() = maxOf(z1, z2)
fun contains(x: Double, y: Double, z: Double): Boolean { fun contains(x: Double, y: Double, z: Double): Boolean {
// Add 0.5 to block coordinates for center-based comparison return x >= minX() && x <= maxX() &&
return x >= minX() + 0.5 && x <= maxX() + 0.5 && y >= minY() && y <= maxY() &&
y >= minY() + 0.5 && y <= maxY() + 0.5 && z >= minZ() && z <= maxZ()
z >= minZ() + 0.5 && z <= maxZ() + 0.5
}
/**
* Returns the two corner blocks (min and max corners) for visualization
*/
fun getCornerBlocks(): Pair<Triple<Int, Int, Int>, Triple<Int, Int, Int>> {
val minCorner = Triple(minX(), minY(), minZ())
val maxCorner = Triple(maxX(), maxY(), maxZ())
return minCorner to maxCorner
} }
} }
@Serializable @Serializable
@SerialName("cylinder") @SerialName("cylinder")
data class Cylinder( data class Cylinder(
val x: Int, val y: Int, val z: Int, val x: Double, val y: Double, val z: Double,
val radius: Double, val radius: Double,
val bottomHeight: Int, // Height extending downward from center block val height: Double // Assuming vertical cylinder
val topHeight: Int // Height extending upward from center block
) : Shape() { ) : Shape() {
fun contains(tx: Double, ty: Double, tz: Double): Boolean { fun contains(tx: Double, ty: Double, tz: Double): Boolean {
// Add 0.5 to block coordinates for center-based comparison if (ty < y || ty > y + height) return false
val centerX = x + 0.5 val dx = tx - x
val centerY = y + 0.5 val dz = tz - z
val centerZ = z + 0.5
// Total height includes center block (1) + bottomHeight + topHeight
val minY = centerY - bottomHeight
val maxY = centerY + 1.0 + topHeight
if (ty < minY || ty > maxY) return false
val dx = tx - centerX
val dz = tz - centerZ
return (dx * dx + dz * dz) <= (radius * radius) return (dx * dx + dz * dz) <= (radius * radius)
} }
/**
* Returns the center block for visualization
*/
fun getCenterBlock(): Triple<Int, Int, Int> {
return Triple(x, y, z)
}
/**
* Total height of the cylinder including center block
*/
fun totalHeight(): Int = 1 + bottomHeight + topHeight
/**
* Returns a set of blocks that form the outline of the blockified cylinder.
* This includes blocks at the edge of the cylinder's radius at each Y level.
*/
fun getBlockifiedOutline(): Set<Triple<Int, Int, Int>> {
val outline = mutableSetOf<Triple<Int, Int, Int>>()
val centerX = x + 0.5
val centerZ = z + 0.5
val radiusSquared = radius * radius
// Calculate the bounding box for the cylinder
val minBlockX = (x - radius - 1).toInt()
val maxBlockX = (x + radius + 1).toInt()
val minBlockZ = (z - radius - 1).toInt()
val maxBlockZ = (z + radius + 1).toInt()
// For each Y level in the cylinder
val minY = y - bottomHeight
val maxY = y + topHeight
for (blockY in minY..maxY) {
// Check each block in the XZ plane
for (blockX in minBlockX..maxBlockX) {
for (blockZ in minBlockZ..maxBlockZ) {
// Check if this block is inside the cylinder
val dx = (blockX + 0.5) - centerX
val dz = (blockZ + 0.5) - centerZ
val isInside = (dx * dx + dz * dz) <= radiusSquared
if (isInside) {
// Check if any adjacent block is outside (making this an edge block)
val hasOutsideNeighbor = listOf(
Pair(blockX - 1, blockZ),
Pair(blockX + 1, blockZ),
Pair(blockX, blockZ - 1),
Pair(blockX, blockZ + 1)
).any { (nx, nz) ->
val ndx = (nx + 0.5) - centerX
val ndz = (nz + 0.5) - centerZ
(ndx * ndx + ndz * ndz) > radiusSquared
}
if (hasOutsideNeighbor) {
outline.add(Triple(blockX, blockY, blockZ))
} }
} }
}
}
}
return outline
}
}
}
/**
* Normalizes a coordinate to block integer (floor)
*/
fun normalizeToBlockInt(coord: Double): Int {
return kotlin.math.floor(coord).toInt()
}

View File

@ -51,158 +51,40 @@ class VisualizerTask(private val landService: LandService) : Runnable {
} }
private fun drawCuboid(player: Player, cuboid: Shape.Cuboid) { private fun drawCuboid(player: Player, cuboid: Shape.Cuboid) {
val (minCorner, maxCorner) = cuboid.getCornerBlocks() // Draw edges
val minX = cuboid.minX()
val maxX = cuboid.maxX()
val minY = cuboid.minY()
val maxY = cuboid.maxY()
val minZ = cuboid.minZ()
val maxZ = cuboid.maxZ()
// Draw outline of min corner block // Just corners for now or simple lines
drawBlockOutline(player, minCorner.first, minCorner.second, minCorner.third, Color.LIME) // A simple way to visualize is to spawn particles at corners and periodically along edges
// Doing full wireframe every tick is heavy.
// Draw outline of max corner block // Let's do corners.
drawBlockOutline(player, maxCorner.first, maxCorner.second, maxCorner.third, Color.YELLOW) val corners = listOf(
Triple(minX, minY, minZ), Triple(maxX, minY, minZ),
// Draw edges of the cuboid using outer corners Triple(minX, maxY, minZ), Triple(maxX, maxY, minZ),
// Min corner is at block position, max corner extends to outer edge (+1.0) Triple(minX, minY, maxZ), Triple(maxX, minY, maxZ),
val minX = cuboid.minX().toDouble() Triple(minX, maxY, maxZ), Triple(maxX, maxY, maxZ)
val maxX = cuboid.maxX() + 1.0 )
val minY = cuboid.minY().toDouble() corners.forEach { (x, y, z) ->
val maxY = cuboid.maxY() + 1.0 player.spawnParticle(Particle.DUST, x, y, z, 1, 0.0, 0.0, 0.0, Particle.DustOptions(Color.LIME, 1.0f))
val minZ = cuboid.minZ().toDouble() }
val maxZ = cuboid.maxZ() + 1.0
val edgeColor = Particle.DustOptions(Color.fromRGB(100, 255, 100), 0.75f)
val step = 0.5
// Bottom face edges
drawLine(player, minX, minY, minZ, maxX, minY, minZ, edgeColor, step)
drawLine(player, maxX, minY, minZ, maxX, minY, maxZ, edgeColor, step)
drawLine(player, maxX, minY, maxZ, minX, minY, maxZ, edgeColor, step)
drawLine(player, minX, minY, maxZ, minX, minY, minZ, edgeColor, step)
// Top face edges
drawLine(player, minX, maxY, minZ, maxX, maxY, minZ, edgeColor, step)
drawLine(player, maxX, maxY, minZ, maxX, maxY, maxZ, edgeColor, step)
drawLine(player, maxX, maxY, maxZ, minX, maxY, maxZ, edgeColor, step)
drawLine(player, minX, maxY, maxZ, minX, maxY, minZ, edgeColor, step)
// Vertical edges
drawLine(player, minX, minY, minZ, minX, maxY, minZ, edgeColor, step)
drawLine(player, maxX, minY, minZ, maxX, maxY, minZ, edgeColor, step)
drawLine(player, maxX, minY, maxZ, maxX, maxY, maxZ, edgeColor, step)
drawLine(player, minX, minY, maxZ, minX, maxY, maxZ, edgeColor, step)
} }
private fun drawCylinder(player: Player, cylinder: Shape.Cylinder) { private fun drawCylinder(player: Player, cylinder: Shape.Cylinder) {
val center = cylinder.getCenterBlock() val segments = 20
// Draw outline of center block
drawBlockOutline(player, center.first, center.second, center.third, Color.AQUA)
// Draw cylinder edges (top and bottom circles + vertical lines)
// Add 0.5 to block coordinates for center on X/Z axis
val centerX = cylinder.x + 0.5
val centerZ = cylinder.z + 0.5
// Y coordinates use block boundaries (no +0.5 offset)
val bottomY = cylinder.y.toDouble() - cylinder.bottomHeight
val topY = cylinder.y.toDouble() + 1.0 + cylinder.topHeight
val segments = 32
val step = (Math.PI * 2) / segments val step = (Math.PI * 2) / segments
val edgeColor = Particle.DustOptions(Color.fromRGB(100, 200, 255), 0.75f) // Top and Bottom circles
// Bottom circle
for (i in 0 until segments) { for (i in 0 until segments) {
val angle1 = i * step val angle = i * step
val angle2 = (i + 1) * step val dx = cos(angle) * cylinder.radius
val x1 = centerX + cos(angle1) * cylinder.radius val dz = sin(angle) * cylinder.radius
val z1 = centerZ + sin(angle1) * cylinder.radius
val x2 = centerX + cos(angle2) * cylinder.radius
val z2 = centerZ + sin(angle2) * cylinder.radius
drawLine(player, x1, bottomY, z1, x2, bottomY, z2, edgeColor, 0.25) player.spawnParticle(Particle.DUST, cylinder.x + dx, cylinder.y, cylinder.z + dz, 1, 0.0, 0.0, 0.0, Particle.DustOptions(Color.AQUA, 1.0f))
} player.spawnParticle(Particle.DUST, cylinder.x + dx, cylinder.y + cylinder.height, cylinder.z + dz, 1, 0.0, 0.0, 0.0, Particle.DustOptions(Color.AQUA, 1.0f))
// Top circle
for (i in 0 until segments) {
val angle1 = i * step
val angle2 = (i + 1) * step
val x1 = centerX + cos(angle1) * cylinder.radius
val z1 = centerZ + sin(angle1) * cylinder.radius
val x2 = centerX + cos(angle2) * cylinder.radius
val z2 = centerZ + sin(angle2) * cylinder.radius
drawLine(player, x1, topY, z1, x2, topY, z2, edgeColor, 0.25)
}
// Vertical lines (8 cardinal directions)
val verticalSegments = 8
for (i in 0 until verticalSegments) {
val angle = i * (Math.PI * 2) / verticalSegments
val x = centerX + cos(angle) * cylinder.radius
val z = centerZ + sin(angle) * cylinder.radius
drawLine(player, x, bottomY, z, x, topY, z, edgeColor, 0.5)
}
// Draw blockified outline
val blockifiedOutline = cylinder.getBlockifiedOutline()
val blockColor = Particle.DustOptions(Color.fromRGB(150, 220, 255), 0.5f)
for ((bx, by, bz) in blockifiedOutline) {
// Draw a subtle outline for each block in the blockified cylinder
drawBlockOutline(player, bx, by, bz, Color.fromRGB(150, 220, 255))
}
}
/**
* Draws an outline of a block using particles
*/
private fun drawBlockOutline(player: Player, x: Int, y: Int, z: Int, color: Color) {
val dustOptions = Particle.DustOptions(color, 1.0f)
val step = 0.25 // Particle spacing along edges
// Bottom face edges
drawLine(player, x.toDouble(), y.toDouble(), z.toDouble(), x + 1.0, y.toDouble(), z.toDouble(), dustOptions, step)
drawLine(player, x + 1.0, y.toDouble(), z.toDouble(), x + 1.0, y.toDouble(), z + 1.0, dustOptions, step)
drawLine(player, x + 1.0, y.toDouble(), z + 1.0, x.toDouble(), y.toDouble(), z + 1.0, dustOptions, step)
drawLine(player, x.toDouble(), y.toDouble(), z + 1.0, x.toDouble(), y.toDouble(), z.toDouble(), dustOptions, step)
// Top face edges
drawLine(player, x.toDouble(), y + 1.0, z.toDouble(), x + 1.0, y + 1.0, z.toDouble(), dustOptions, step)
drawLine(player, x + 1.0, y + 1.0, z.toDouble(), x + 1.0, y + 1.0, z + 1.0, dustOptions, step)
drawLine(player, x + 1.0, y + 1.0, z + 1.0, x.toDouble(), y + 1.0, z + 1.0, dustOptions, step)
drawLine(player, x.toDouble(), y + 1.0, z + 1.0, x.toDouble(), y + 1.0, z.toDouble(), dustOptions, step)
// Vertical edges
drawLine(player, x.toDouble(), y.toDouble(), z.toDouble(), x.toDouble(), y + 1.0, z.toDouble(), dustOptions, step)
drawLine(player, x + 1.0, y.toDouble(), z.toDouble(), x + 1.0, y + 1.0, z.toDouble(), dustOptions, step)
drawLine(player, x + 1.0, y.toDouble(), z + 1.0, x + 1.0, y + 1.0, z + 1.0, dustOptions, step)
drawLine(player, x.toDouble(), y.toDouble(), z + 1.0, x.toDouble(), y + 1.0, z + 1.0, dustOptions, step)
}
/**
* Draws a line of particles between two points
*/
private fun drawLine(
player: Player,
x1: Double, y1: Double, z1: Double,
x2: Double, y2: Double, z2: Double,
dustOptions: Particle.DustOptions,
step: Double
) {
val dx = x2 - x1
val dy = y2 - y1
val dz = z2 - z1
val distance = kotlin.math.sqrt(dx * dx + dy * dy + dz * dz)
val steps = (distance / step).toInt()
if (steps == 0) return
for (i in 0..steps) {
val t = i.toDouble() / steps
val x = x1 + dx * t
val y = y1 + dy * t
val z = z1 + dz * t
player.spawnParticle(Particle.DUST, x, y, z, 1, 0.0, 0.0, 0.0, dustOptions)
} }
} }
} }