From 8fcacf37ff95c1bfaa503e5e724d998d2b19acd6 Mon Sep 17 00:00:00 2001 From: Hare Date: Tue, 9 Dec 2025 07:09:29 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=E3=83=87=E3=83=BC=E3=82=BF=E5=BD=A2?= =?UTF-8?q?=E5=BC=8F=E3=81=AE=E4=BF=AE=E6=AD=A3=E3=81=A8visualize?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hcu-core | 1 + .../hcu/lands/command/LandsCommand.kt | 247 ++++++++++++++++-- .../net/hareworks/hcu/lands/model/Land.kt | 59 ++++- .../hcu/lands/task/VisualizerTask.kt | 163 ++++++++++-- 4 files changed, 405 insertions(+), 65 deletions(-) create mode 160000 hcu-core diff --git a/hcu-core b/hcu-core new file mode 160000 index 0000000..520a378 --- /dev/null +++ b/hcu-core @@ -0,0 +1 @@ +Subproject commit 520a378cd5c3da6321305d07cc0b402b73292add diff --git a/src/main/kotlin/net/hareworks/hcu/lands/command/LandsCommand.kt b/src/main/kotlin/net/hareworks/hcu/lands/command/LandsCommand.kt index 41b8268..b2f3d2f 100644 --- a/src/main/kotlin/net/hareworks/hcu/lands/command/LandsCommand.kt +++ b/src/main/kotlin/net/hareworks/hcu/lands/command/LandsCommand.kt @@ -4,6 +4,7 @@ import io.papermc.paper.math.Position import net.hareworks.hcu.lands.App import net.hareworks.hcu.lands.model.Land import net.hareworks.hcu.lands.model.Shape +import net.hareworks.hcu.lands.model.normalizeToBlockInt import net.hareworks.hcu.lands.task.LandsVisualizer import net.hareworks.kommand_lib.kommand import net.kyori.adventure.text.Component @@ -88,7 +89,14 @@ class LandsCommand( executes { val p1: Position = argument("pos1") val p2: Position = argument("pos2") - val shape = Shape.Cuboid(p1.x(), p1.y(), p1.z(), p2.x(), p2.y(), p2.z()) + val shape = Shape.Cuboid( + normalizeToBlockInt(p1.x()), + normalizeToBlockInt(p1.y()), + normalizeToBlockInt(p1.z()), + normalizeToBlockInt(p2.x()), + normalizeToBlockInt(p2.y()), + normalizeToBlockInt(p2.z()) + ) val name: String = argument("name") updateLandData(sender, name) { parts -> @@ -102,17 +110,99 @@ class LandsCommand( literal("cylinder") { blockCoordinates("center") { float("radius") { - float("height") { - executes { - val c: Position = argument("center") - val r: Double = argument("radius") - val h: Double = argument("height") - val shape = Shape.Cylinder(c.x(), c.y(), c.z(), r, h) - val name: String = argument("name") - - updateLandData(sender, name) { parts -> - parts.add(shape) - sender.sendMessage(Component.text("Shape added.", NamedTextColor.GREEN)) + literal("center") { + integer("totalHeight", min = 1) { + executes { + val c: Position = argument("center") + val r: Double = argument("radius") + val total: Int = argument("totalHeight") + 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 -> + parts.add(shape) + sender.sendMessage(Component.text("Shape added.", NamedTextColor.GREEN)) + } + } + } + } + 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)) + } } } } @@ -130,7 +220,14 @@ class LandsCommand( val index: Int = argument("index") val p1: Position = argument("pos1") val p2: Position = argument("pos2") - val shape = Shape.Cuboid(p1.x(), p1.y(), p1.z(), p2.x(), p2.y(), p2.z()) + val shape = Shape.Cuboid( + normalizeToBlockInt(p1.x()), + normalizeToBlockInt(p1.y()), + normalizeToBlockInt(p1.z()), + normalizeToBlockInt(p2.x()), + normalizeToBlockInt(p2.y()), + normalizeToBlockInt(p2.z()) + ) val name: String = argument("name") updateLandData(sender, name) { parts -> @@ -148,21 +245,115 @@ class LandsCommand( literal("cylinder") { blockCoordinates("center") { float("radius") { - float("height") { - executes { - val index: Int = argument("index") - val c: Position = argument("center") - val r: Double = argument("radius") - val h: Double = argument("height") - val shape = Shape.Cylinder(c.x(), c.y(), c.z(), r, h) - val name: String = argument("name") - - 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("center") { + integer("totalHeight", min = 1) { + executes { + val index: Int = argument("index") + val c: Position = argument("center") + val r: Double = argument("radius") + val total: Int = argument("totalHeight") + 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 -> + 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("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)) + } } } } diff --git a/src/main/kotlin/net/hareworks/hcu/lands/model/Land.kt b/src/main/kotlin/net/hareworks/hcu/lands/model/Land.kt index fc63892..c78ac44 100644 --- a/src/main/kotlin/net/hareworks/hcu/lands/model/Land.kt +++ b/src/main/kotlin/net/hareworks/hcu/lands/model/Land.kt @@ -21,8 +21,8 @@ sealed class Shape { @Serializable @SerialName("cuboid") data class Cuboid( - val x1: Double, val y1: Double, val z1: Double, - val x2: Double, val y2: Double, val z2: Double + val x1: Int, val y1: Int, val z1: Int, + val x2: Int, val y2: Int, val z2: Int ) : Shape() { fun minX() = minOf(x1, x2) fun maxX() = maxOf(x1, x2) @@ -32,24 +32,63 @@ sealed class Shape { fun maxZ() = maxOf(z1, z2) fun contains(x: Double, y: Double, z: Double): Boolean { - return x >= minX() && x <= maxX() && - y >= minY() && y <= maxY() && - z >= minZ() && z <= maxZ() + // Add 0.5 to block coordinates for center-based comparison + return x >= minX() + 0.5 && x <= maxX() + 0.5 && + y >= minY() + 0.5 && y <= maxY() + 0.5 && + z >= minZ() + 0.5 && z <= maxZ() + 0.5 + } + + /** + * Returns the two corner blocks (min and max corners) for visualization + */ + fun getCornerBlocks(): Pair, Triple> { + val minCorner = Triple(minX(), minY(), minZ()) + val maxCorner = Triple(maxX(), maxY(), maxZ()) + return minCorner to maxCorner } } @Serializable @SerialName("cylinder") data class Cylinder( - val x: Double, val y: Double, val z: Double, + val x: Int, val y: Int, val z: Int, val radius: Double, - val height: Double // Assuming vertical cylinder + val bottomHeight: Int, // Height extending downward from center block + val topHeight: Int // Height extending upward from center block ) : Shape() { fun contains(tx: Double, ty: Double, tz: Double): Boolean { - if (ty < y || ty > y + height) return false - val dx = tx - x - val dz = tz - z + // Add 0.5 to block coordinates for center-based comparison + val centerX = x + 0.5 + val centerY = y + 0.5 + 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) } + + /** + * Returns the center block for visualization + */ + fun getCenterBlock(): Triple { + return Triple(x, y, z) + } + + /** + * Total height of the cylinder including center block + */ + fun totalHeight(): Int = 1 + bottomHeight + topHeight } } + +/** + * Normalizes a coordinate to block integer (floor) + */ +fun normalizeToBlockInt(coord: Double): Int { + return kotlin.math.floor(coord).toInt() +} diff --git a/src/main/kotlin/net/hareworks/hcu/lands/task/VisualizerTask.kt b/src/main/kotlin/net/hareworks/hcu/lands/task/VisualizerTask.kt index a05ee46..ead7ac0 100644 --- a/src/main/kotlin/net/hareworks/hcu/lands/task/VisualizerTask.kt +++ b/src/main/kotlin/net/hareworks/hcu/lands/task/VisualizerTask.kt @@ -51,40 +51,149 @@ class VisualizerTask(private val landService: LandService) : Runnable { } private fun drawCuboid(player: Player, cuboid: Shape.Cuboid) { - // 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() + val (minCorner, maxCorner) = cuboid.getCornerBlocks() - // Just corners for now or simple lines - // A simple way to visualize is to spawn particles at corners and periodically along edges - // Doing full wireframe every tick is heavy. - // Let's do corners. - val corners = listOf( - Triple(minX, minY, minZ), Triple(maxX, minY, minZ), - Triple(minX, maxY, minZ), Triple(maxX, maxY, minZ), - Triple(minX, minY, maxZ), Triple(maxX, minY, maxZ), - Triple(minX, maxY, maxZ), Triple(maxX, maxY, maxZ) - ) - corners.forEach { (x, y, z) -> - player.spawnParticle(Particle.DUST, x, y, z, 1, 0.0, 0.0, 0.0, Particle.DustOptions(Color.LIME, 1.0f)) - } + // Draw outline of min corner block + drawBlockOutline(player, minCorner.first, minCorner.second, minCorner.third, Color.LIME) + + // Draw outline of max corner block + drawBlockOutline(player, maxCorner.first, maxCorner.second, maxCorner.third, Color.YELLOW) + + // Draw edges of the cuboid using outer corners + // Min corner is at block position, max corner extends to outer edge (+1.0) + val minX = cuboid.minX().toDouble() + val maxX = cuboid.maxX() + 1.0 + val minY = cuboid.minY().toDouble() + val maxY = cuboid.maxY() + 1.0 + 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) { - val segments = 20 + val center = cylinder.getCenterBlock() + + // 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 - // Top and Bottom circles + val edgeColor = Particle.DustOptions(Color.fromRGB(100, 200, 255), 0.75f) + + // Bottom circle for (i in 0 until segments) { - val angle = i * step - val dx = cos(angle) * cylinder.radius - val dz = sin(angle) * cylinder.radius + 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 - 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)) + drawLine(player, x1, bottomY, z1, x2, bottomY, z2, edgeColor, 0.25) + } + + // 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) + } + } + + /** + * 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) } } }