diff --git a/build.gradle.kts b/build.gradle.kts index 726d9f7..cb90054 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -18,7 +18,7 @@ val exposedVersion = "1.0.0-rc-4" dependencies { compileOnly("io.papermc.paper:paper-api:1.21.10-R0.1-SNAPSHOT") - implementation("org.jetbrains.kotlin:kotlin-stdlib:2.1.0") + compileOnly("org.jetbrains.kotlin:kotlin-stdlib:2.1.0") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0") { exclude(group = "org.jetbrains.kotlin") } @@ -49,7 +49,13 @@ tasks { exclude(dependency("net.hareworks:kommand-lib")) exclude(dependency("net.hareworks:permits-lib")) } - relocate("kotlin", "net.hareworks.hcu.lands.libs.kotlin") + dependencies { + exclude(dependency("org.jetbrains.kotlin:kotlin-stdlib")) + exclude(dependency("org.jetbrains.kotlin:kotlin-reflect")) + exclude(dependency("org.jetbrains.kotlin:kotlin-stdlib-jdk8")) + exclude(dependency("org.jetbrains.kotlin:kotlin-stdlib-jdk7")) + } + relocate("net.hareworks.kommand_lib", "net.hareworks.hcu.lands.libs.kommand_lib") relocate("net.hareworks.permits_lib", "net.hareworks.hcu.lands.libs.permits_lib") } 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 9127d5c..eebe1b8 100644 --- a/src/main/kotlin/net/hareworks/hcu/lands/task/VisualizerTask.kt +++ b/src/main/kotlin/net/hareworks/hcu/lands/task/VisualizerTask.kt @@ -10,11 +10,17 @@ import org.bukkit.Location import org.bukkit.Particle import org.bukkit.entity.Player import org.bukkit.entity.TextDisplay +import org.bukkit.util.Transformation +import org.bukkit.util.Vector +import org.joml.AxisAngle4f +import org.joml.Vector3f import java.util.UUID import java.util.concurrent.ConcurrentHashMap import kotlin.math.abs +import kotlin.math.atan2 import kotlin.math.cos import kotlin.math.sin +import kotlin.math.sqrt object LandsVisualizer { private val enabledPlayers = mutableSetOf() @@ -203,6 +209,27 @@ class VisualizerTask( drawLine(player, x, bottomY, z, x, topY, z, edgeColor, 0.5) } + + // Draw block-aligned boundary (Red) + val faceBlocks = mutableSetOf>() + val cY = cylinder.y + 0.5 + val rCheck = cylinder.radius + 2 // Check sufficient area + val centerBlock = cylinder.getCenterBlock() + val cBx = centerBlock.first + val cBz = centerBlock.third + + for (x in (cBx - rCheck).toInt()..(cBx + rCheck).toInt()) { + for (z in (cBz - rCheck).toInt()..(cBz + rCheck).toInt()) { + // Check center of block + if (cylinder.contains(x + 0.5, cY, z + 0.5)) { + faceBlocks.add(Pair(x, z)) + } + } + } + + val redDust = Particle.DustOptions(Color.RED, 1.0f) + drawFaceOutline(player, faceBlocks, bottomY, redDust, cylinder) + drawFaceOutline(player, faceBlocks, topY, redDust, cylinder) } /** @@ -305,80 +332,261 @@ class VisualizerTask( } private fun createDisplayForLand(player: Player, land: net.hareworks.hcu.lands.model.Land): DisplayController { - val position = calculateFaceAttachmentPoint(player, land) + val position = calculateBestAttachmentLocation(player, land) val controller = displayService!!.createTextDisplay(position) controller.applyEntityUpdate { display -> display.text(net.kyori.adventure.text.Component.text(land.name)) - display.billboard = org.bukkit.entity.Display.Billboard.VERTICAL + display.billboard = org.bukkit.entity.Display.Billboard.FIXED + display.transformation = Transformation( + Vector3f(), + AxisAngle4f(), + Vector3f(6f, 6f, 6f), + AxisAngle4f() + ) } controller.show(player) - logger.info("[LandsDisplay] Display for '${land.name}' made visible to ${player.name} at ${position.blockX}, ${position.blockY}, ${position.blockZ}") + // logger.info("[LandsDisplay] Display for '${land.name}' made visible to ${player.name}") return controller } private fun updateDisplayPosition(controller: DisplayController, player: Player, land: net.hareworks.hcu.lands.model.Land) { - val newPosition = calculateFaceAttachmentPoint(player, land) + val newPosition = calculateBestAttachmentLocation(player, land) val currentLoc = controller.display.location - // Only update if position changed significantly (>0.5 blocks) - if (currentLoc.distanceSquared(newPosition) > 0.25) { + // Update if position or rotation (yaw) changed significantly + if (currentLoc.distanceSquared(newPosition) > 0.05 || abs(currentLoc.yaw - newPosition.yaw) > 1.0) { controller.applyEntityUpdate { display -> + // teleport handles both position and rotation display.teleport(newPosition) } } } - private fun calculateFaceAttachmentPoint(player: Player, land: net.hareworks.hcu.lands.model.Land): Location { - val bbox = land.getBoundingBox() - val px = player.location.x - val py = player.location.y - val pz = player.location.z + private fun calculateBestAttachmentLocation(player: Player, land: net.hareworks.hcu.lands.model.Land): Location { + var bestLoc: Location? = null + var minDistSq = Double.MAX_VALUE - // Check if player is within XZ footprint - val withinX = px >= bbox.minX && px <= bbox.maxX + 1.0 - val withinZ = pz >= bbox.minZ && pz <= bbox.maxZ + 1.0 - - if (withinX && withinZ) { - // Player is above or below the land → use top/bottom face - return if (py > bbox.maxY + 1.0) { - // Attach to top face - Location( - player.world, - px.coerceIn(bbox.minX.toDouble(), bbox.maxX + 1.0), - bbox.maxY + 1.1, - pz.coerceIn(bbox.minZ.toDouble(), bbox.maxZ + 1.0) - ) - } else { - // Attach to bottom face - Location( - player.world, - px.coerceIn(bbox.minX.toDouble(), bbox.maxX + 1.0), - bbox.minY - 0.1, - pz.coerceIn(bbox.minZ.toDouble(), bbox.maxZ + 1.0) - ) + // Find closest point among all shapes + for (shape in land.data.parts) { + val loc = getAttachmentPointOnShape(player, shape) + val distSq = loc.distanceSquared(player.location) + if (distSq < minDistSq) { + minDistSq = distSq + bestLoc = loc } } - // Player is outside XZ footprint → use nearest vertical face - val clampedX = px.coerceIn(bbox.minX.toDouble(), bbox.maxX + 1.0) - val clampedY = py.coerceIn(bbox.minY.toDouble(), bbox.maxY + 1.0) - val clampedZ = pz.coerceIn(bbox.minZ.toDouble(), bbox.maxZ + 1.0) - - // Determine which face is nearest - val distToWest = abs(px - bbox.minX) - val distToEast = abs(px - (bbox.maxX + 1.0)) - val distToNorth = abs(pz - bbox.minZ) - val distToSouth = abs(pz - (bbox.maxZ + 1.0)) - - val minDist = minOf(distToWest, distToEast, distToNorth, distToSouth) - - return when (minDist) { - distToWest -> Location(player.world, bbox.minX - 0.1, clampedY, clampedZ) - distToEast -> Location(player.world, bbox.maxX + 1.1, clampedY, clampedZ) - distToNorth -> Location(player.world, clampedX, clampedY, bbox.minZ - 0.1) - else -> Location(player.world, clampedX, clampedY, bbox.maxZ + 1.1) + return bestLoc ?: player.location + } + + private fun getAttachmentPointOnShape(player: Player, shape: Shape): Location { + return when (shape) { + is Shape.Cuboid -> getCuboidAttachment(player, shape) + is Shape.Cylinder -> getCylinderAttachment(player, shape) } } + + private fun getCuboidAttachment(player: Player, cuboid: Shape.Cuboid): Location { + val px = player.location.x + val py = player.location.y + val pz = player.location.z + + // Cuboid bounds + 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 + + // Determine if player is inside the extended bounding box (XZ) + val insideX = px in minX..maxX + val insideZ = pz in minZ..maxZ + val insideY = py in minY..maxY + + val margin = 0.1 + + var finalX = px.coerceIn(minX, maxX) + var finalY = py.coerceIn(minY, maxY) + var finalZ = pz.coerceIn(minZ, maxZ) + + var normal = Vector(0, 1, 0) // Default up + + if (insideX && insideZ && insideY) { + // Inside: find nearest wall/floor/ceiling + // And snap position to that wall, facing INWARDS + val dWest = abs(px - minX) + val dEast = abs(px - maxX) + val dDown = abs(py - minY) + val dUp = abs(py - maxY) + val dNorth= abs(pz - minZ) + val dSouth= abs(pz - maxZ) + + val minD = minOf(dWest, dEast, dDown, dUp, dNorth, dSouth) + + when (minD) { + dWest -> { + finalX = minX + normal = Vector(1, 0, 0) + } + dEast -> { + finalX = maxX + normal = Vector(-1, 0, 0) + } + dDown -> { + finalY = minY + normal = Vector(0, 1, 0) + } + dUp -> { + finalY = maxY + normal = Vector(0, -1, 0) + } + dNorth -> { + finalZ = minZ + normal = Vector(0, 0, 1) + } + dSouth -> { + finalZ = maxZ + normal = Vector(0, 0, -1) + } + } + } else { + // Outside: calculate vector from clamped point to player? + // "Stick to face orientation" -> The text should lie ON the face. + // If we are closest to the East face (outside), we want Normal = (1,0,0) so text faces East. + + // Determine which face we are closest to / projected onto. + val dx = if (px < minX) minX - px else if (px > maxX) px - maxX else 0.0 + val dy = if (py < minY) minY - py else if (py > maxY) py - maxY else 0.0 + val dz = if (pz < minZ) minZ - pz else if (pz > maxZ) pz - maxZ else 0.0 + + if (dx == 0.0 && dy == 0.0 && dz == 0.0) { + // Should not happen if strictly outside, but fallback + normal = Vector(0, 1, 0) + } else { + // dominant axis + if (dx >= dy && dx >= dz) { + normal = Vector(if (px < minX) -1 else 1, 0, 0) + } else if (dy >= dx && dy >= dz) { + normal = Vector(0, if (py < minY) -1 else 1, 0) + } else { + normal = Vector(0, 0, if (pz < minZ) -1 else 1) + } + } + } + + // Apply offset based on normal so it sits slightly outside the block (or inside if inverted) + val offsetPos = Vector(finalX, finalY, finalZ).add(normal.clone().multiply(margin)) + + val result = Location(player.world, offsetPos.x, offsetPos.y, offsetPos.z) + if (normal.lengthSquared() > 0) { + result.direction = normal + } + return result + } + + private fun getCylinderAttachment(player: Player, cyl: Shape.Cylinder): Location { + val px = player.location.x + val py = player.location.y + val pz = player.location.z + + // Cylinder definition + val cx = cyl.x + 0.5 + val cz = cyl.z + 0.5 + val minY = cyl.y.toDouble() - cyl.bottomHeight + val maxY = cyl.y.toDouble() + 1.0 + cyl.topHeight + val radius = cyl.radius + 0.5 // Visual radius + + val dx = px - cx + val dz = pz - cz + val distHorizontal = sqrt(dx*dx + dz*dz) + + val margin = 0.1 + + // Check if strictly inside the cylinder volume + val insideY = py in minY..maxY + val insideHorizontal = distHorizontal < radius + + var normal = Vector(0, 1, 0) + var targetX = px + var targetY = py + var targetZ = pz + + if (insideHorizontal && insideY) { + // Inside volume. Push to nearest surface (Top, Bottom, or Side) + // And face INWARDS (towards center/player) + val dSide = radius - distHorizontal + val dTop = maxY - py + val dBottom = py - minY + + val minD = minOf(dSide, dTop, dBottom) + + if (minD == dTop) { + targetY = maxY + normal = Vector(0, -1, 0) // Face Down (Inwards) + } else if (minD == dBottom) { + targetY = minY + normal = Vector(0, 1, 0) // Face Up (Inwards) + } else { + // Side + if (distHorizontal > 0.001) { + val factor = radius / distHorizontal + targetX = cx + dx * factor + targetZ = cz + dz * factor + // Normal towards center + normal = Vector(-dx, 0.0, -dz).normalize() + } else { + // Dead center? push X+ and face West + targetX = cx + radius + targetZ = cz + normal = Vector(-1, 0, 0) + } + } + } else { + // Outside (or above/below). + // Clamp Y + targetY = py.coerceIn(minY, maxY) + + // Horizontal logic + // We want the point on the cylinder surface closest to player + if (insideHorizontal) { + // Player is above/below, but within radius. + // Normal is Up or Down (Outwards) + targetX = px + targetZ = pz + normal = if (py > maxY) Vector(0, 1, 0) else Vector(0, -1, 0) + if (py > maxY) targetY = maxY else targetY = minY + } else { + // Outside radius. Closest point is on circumference. + if (distHorizontal > 0.001) { + val factor = radius / distHorizontal + targetX = cx + dx * factor + targetZ = cz + dz * factor + + val dY = if (py < minY) minY - py else if (py > maxY) py - maxY else 0.0 + val dSide = distHorizontal - radius + + if (dY > dSide) { + // Closer to cap + normal = if (py > maxY) Vector(0, 1, 0) else Vector(0, -1, 0) + } else { + // Closer to side + normal = Vector(dx, 0.0, dz).normalize() + } + } else { + normal = Vector(1, 0, 0) // Should not happen if outside radius + } + } + } + + // Apply offset + val finalPos = Vector(targetX, targetY, targetZ).add(normal.clone().multiply(margin)) + val result = Location(player.world, finalPos.x, finalPos.y, finalPos.z) + if (normal.lengthSquared() > 0) { + result.direction = normal + } + return result + } fun cleanupDisplaysForPlayer(playerId: UUID) { val count = displayControllers[playerId]?.size ?: 0