feat: GhostDisplayを利用したname表示

This commit is contained in:
Keisuke Hirata 2025-12-12 01:10:14 +09:00
parent 675de7b2ed
commit 71e64108c7
2 changed files with 267 additions and 53 deletions

View File

@ -18,7 +18,7 @@ val exposedVersion = "1.0.0-rc-4"
dependencies { dependencies {
compileOnly("io.papermc.paper:paper-api:1.21.10-R0.1-SNAPSHOT") 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") { implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0") {
exclude(group = "org.jetbrains.kotlin") exclude(group = "org.jetbrains.kotlin")
} }
@ -49,7 +49,13 @@ tasks {
exclude(dependency("net.hareworks:kommand-lib")) exclude(dependency("net.hareworks:kommand-lib"))
exclude(dependency("net.hareworks:permits-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.kommand_lib", "net.hareworks.hcu.lands.libs.kommand_lib")
relocate("net.hareworks.permits_lib", "net.hareworks.hcu.lands.libs.permits_lib") relocate("net.hareworks.permits_lib", "net.hareworks.hcu.lands.libs.permits_lib")
} }

View File

@ -10,11 +10,17 @@ import org.bukkit.Location
import org.bukkit.Particle import org.bukkit.Particle
import org.bukkit.entity.Player import org.bukkit.entity.Player
import org.bukkit.entity.TextDisplay 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.UUID
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.atan2
import kotlin.math.cos import kotlin.math.cos
import kotlin.math.sin import kotlin.math.sin
import kotlin.math.sqrt
object LandsVisualizer { object LandsVisualizer {
private val enabledPlayers = mutableSetOf<UUID>() private val enabledPlayers = mutableSetOf<UUID>()
@ -203,6 +209,27 @@ class VisualizerTask(
drawLine(player, x, bottomY, z, x, topY, z, edgeColor, 0.5) drawLine(player, x, bottomY, z, x, topY, z, edgeColor, 0.5)
} }
// Draw block-aligned boundary (Red)
val faceBlocks = mutableSetOf<Pair<Int, Int>>()
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,79 +332,260 @@ class VisualizerTask(
} }
private fun createDisplayForLand(player: Player, land: net.hareworks.hcu.lands.model.Land): DisplayController<TextDisplay> { private fun createDisplayForLand(player: Player, land: net.hareworks.hcu.lands.model.Land): DisplayController<TextDisplay> {
val position = calculateFaceAttachmentPoint(player, land) val position = calculateBestAttachmentLocation(player, land)
val controller = displayService!!.createTextDisplay(position) val controller = displayService!!.createTextDisplay(position)
controller.applyEntityUpdate { display -> controller.applyEntityUpdate { display ->
display.text(net.kyori.adventure.text.Component.text(land.name)) 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) 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 return controller
} }
private fun updateDisplayPosition(controller: DisplayController<TextDisplay>, player: Player, land: net.hareworks.hcu.lands.model.Land) { private fun updateDisplayPosition(controller: DisplayController<TextDisplay>, player: Player, land: net.hareworks.hcu.lands.model.Land) {
val newPosition = calculateFaceAttachmentPoint(player, land) val newPosition = calculateBestAttachmentLocation(player, land)
val currentLoc = controller.display.location val currentLoc = controller.display.location
// Only update if position changed significantly (>0.5 blocks) // Update if position or rotation (yaw) changed significantly
if (currentLoc.distanceSquared(newPosition) > 0.25) { if (currentLoc.distanceSquared(newPosition) > 0.05 || abs(currentLoc.yaw - newPosition.yaw) > 1.0) {
controller.applyEntityUpdate { display -> controller.applyEntityUpdate { display ->
// teleport handles both position and rotation
display.teleport(newPosition) display.teleport(newPosition)
} }
} }
} }
private fun calculateFaceAttachmentPoint(player: Player, land: net.hareworks.hcu.lands.model.Land): Location { private fun calculateBestAttachmentLocation(player: Player, land: net.hareworks.hcu.lands.model.Land): Location {
val bbox = land.getBoundingBox() var bestLoc: Location? = null
var minDistSq = Double.MAX_VALUE
// 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
}
}
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 px = player.location.x
val py = player.location.y val py = player.location.y
val pz = player.location.z val pz = player.location.z
// Check if player is within XZ footprint // Cuboid bounds
val withinX = px >= bbox.minX && px <= bbox.maxX + 1.0 val minX = cuboid.minX().toDouble()
val withinZ = pz >= bbox.minZ && pz <= bbox.maxZ + 1.0 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
if (withinX && withinZ) { // Determine if player is inside the extended bounding box (XZ)
// Player is above or below the land → use top/bottom face val insideX = px in minX..maxX
return if (py > bbox.maxY + 1.0) { val insideZ = pz in minZ..maxZ
// Attach to top face val insideY = py in minY..maxY
Location(
player.world, val margin = 0.1
px.coerceIn(bbox.minX.toDouble(), bbox.maxX + 1.0),
bbox.maxY + 1.1, var finalX = px.coerceIn(minX, maxX)
pz.coerceIn(bbox.minZ.toDouble(), bbox.maxZ + 1.0) 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 { } else {
// Attach to bottom face // Side
Location( if (distHorizontal > 0.001) {
player.world, val factor = radius / distHorizontal
px.coerceIn(bbox.minX.toDouble(), bbox.maxX + 1.0), targetX = cx + dx * factor
bbox.minY - 0.1, targetZ = cz + dz * factor
pz.coerceIn(bbox.minZ.toDouble(), bbox.maxZ + 1.0) // 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
}
} }
} }
// Player is outside XZ footprint → use nearest vertical face // Apply offset
val clampedX = px.coerceIn(bbox.minX.toDouble(), bbox.maxX + 1.0) val finalPos = Vector(targetX, targetY, targetZ).add(normal.clone().multiply(margin))
val clampedY = py.coerceIn(bbox.minY.toDouble(), bbox.maxY + 1.0) val result = Location(player.world, finalPos.x, finalPos.y, finalPos.z)
val clampedZ = pz.coerceIn(bbox.minZ.toDouble(), bbox.maxZ + 1.0) if (normal.lengthSquared() > 0) {
result.direction = normal
// 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 result
} }
fun cleanupDisplaysForPlayer(playerId: UUID) { fun cleanupDisplaysForPlayer(playerId: UUID) {