feat: GhostDisplayを利用したname表示
This commit is contained in:
parent
675de7b2ed
commit
71e64108c7
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,80 +332,261 @@ 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
|
||||||
val px = player.location.x
|
var minDistSq = Double.MAX_VALUE
|
||||||
val py = player.location.y
|
|
||||||
val pz = player.location.z
|
|
||||||
|
|
||||||
// Check if player is within XZ footprint
|
// Find closest point among all shapes
|
||||||
val withinX = px >= bbox.minX && px <= bbox.maxX + 1.0
|
for (shape in land.data.parts) {
|
||||||
val withinZ = pz >= bbox.minZ && pz <= bbox.maxZ + 1.0
|
val loc = getAttachmentPointOnShape(player, shape)
|
||||||
|
val distSq = loc.distanceSquared(player.location)
|
||||||
if (withinX && withinZ) {
|
if (distSq < minDistSq) {
|
||||||
// Player is above or below the land → use top/bottom face
|
minDistSq = distSq
|
||||||
return if (py > bbox.maxY + 1.0) {
|
bestLoc = loc
|
||||||
// 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)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Player is outside XZ footprint → use nearest vertical face
|
return bestLoc ?: player.location
|
||||||
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)
|
private fun getAttachmentPointOnShape(player: Player, shape: Shape): Location {
|
||||||
|
return when (shape) {
|
||||||
// Determine which face is nearest
|
is Shape.Cuboid -> getCuboidAttachment(player, shape)
|
||||||
val distToWest = abs(px - bbox.minX)
|
is Shape.Cylinder -> getCylinderAttachment(player, shape)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
fun cleanupDisplaysForPlayer(playerId: UUID) {
|
||||||
val count = displayControllers[playerId]?.size ?: 0
|
val count = displayControllers[playerId]?.size ?: 0
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user