feat: GhostDisplayを利用したname表示
This commit is contained in:
parent
675de7b2ed
commit
71e64108c7
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<UUID>()
|
||||
|
|
@ -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<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> {
|
||||
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<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
|
||||
|
||||
// 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()
|
||||
private fun calculateBestAttachmentLocation(player: Player, land: net.hareworks.hcu.lands.model.Land): Location {
|
||||
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 py = player.location.y
|
||||
val pz = player.location.z
|
||||
|
||||
// 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
|
||||
// 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
|
||||
|
||||
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)
|
||||
)
|
||||
// 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 {
|
||||
// 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)
|
||||
)
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
// 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) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user