Skip to content

Commit 86cc987

Browse files
committed
feat: add rotation to text displays
1 parent bd3a9d5 commit 86cc987

File tree

1 file changed

+151
-8
lines changed

1 file changed

+151
-8
lines changed

src/main/kotlin/cc/modlabs/kpaper/visuals/display/TextDisplayManager.kt

Lines changed: 151 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package cc.modlabs.kpaper.visuals.display
22

3+
import cc.modlabs.kpaper.extensions.timer
4+
import cc.modlabs.kpaper.main.PluginInstance
35
import cc.modlabs.kpaper.util.getLogger
46
import com.github.retrooper.packetevents.PacketEvents
57
import com.github.retrooper.packetevents.protocol.entity.data.EntityData
@@ -14,9 +16,12 @@ import com.github.retrooper.packetevents.util.Vector3f
1416
import dev.fruxz.stacked.text
1517
import org.bukkit.Location
1618
import org.bukkit.entity.Player
19+
import org.bukkit.scheduler.BukkitTask
1720
import java.util.*
1821
import java.util.concurrent.ConcurrentHashMap
1922
import java.util.concurrent.atomic.AtomicInteger
23+
import kotlin.math.cos
24+
import kotlin.math.sin
2025

2126
/**
2227
* A manager class for creating and managing packet-based Text Display entities in Minecraft.
@@ -222,6 +227,98 @@ class TextDisplayManager {
222227
}
223228
}
224229

230+
/**
231+
* Updates the transformation of an existing Text Display.
232+
* This includes scale, translation, rotation, and interpolation settings.
233+
*
234+
* @param textDisplay The TextDisplay to update
235+
*/
236+
fun updateTransformation(textDisplay: TextDisplay) {
237+
// Send metadata update for all viewers
238+
val metadataPacket = createMetadataPacket(textDisplay)
239+
for (viewer in textDisplay.viewers) {
240+
PacketEvents.getAPI().playerManager.sendPacket(viewer, metadataPacket)
241+
}
242+
}
243+
244+
/**
245+
* Starts continuous rotation around the Y-axis for a Text Display.
246+
* The display will rotate smoothly using interpolation.
247+
*
248+
* This is similar to PaperMC's transformation matrix rotation, but uses quaternions
249+
* for packet-based entities.
250+
*
251+
* @param textDisplay The TextDisplay to rotate
252+
* @param durationTicks The duration of half a revolution in ticks (default: 100 ticks = 5 seconds)
253+
* @param scale The scale multiplier for the display during rotation (default: 1.0 = no scaling)
254+
*/
255+
fun startRotation(
256+
textDisplay: TextDisplay,
257+
durationTicks: Int = 100,
258+
scale: Float = 1.0f
259+
) {
260+
// Stop any existing rotation
261+
stopRotation(textDisplay)
262+
263+
// Apply scale if specified
264+
if (scale != 1.0f) {
265+
textDisplay.scale = Vector3f(scale, scale, scale)
266+
}
267+
268+
// Initialize rotation angle to 180 degrees + small offset (to prevent reverse interpolation)
269+
textDisplay.currentRotationAngle = Math.toRadians(180.0 + 0.1).toFloat()
270+
271+
// Calculate rotation increment per tick
272+
// Rotate 180 degrees over the duration (half revolution)
273+
val radiansPerTick = Math.toRadians(180.0 / durationTicks).toFloat()
274+
275+
// Set interpolation duration for smooth rotation
276+
textDisplay.transformationInterpolationDuration = durationTicks
277+
textDisplay.interpolationDelay = 0
278+
279+
// Delay initial transformation by one tick (as per PaperMC example)
280+
var isFirstUpdate = true
281+
282+
// Start rotation task - update every tick
283+
textDisplay.rotationTask = timer(1, "TextDisplayRotation-${textDisplay.entityId}") {
284+
// Check if display is still valid and has viewers
285+
if (textDisplay.viewers.isEmpty() || !activeDisplays.containsKey(textDisplay.entityId)) {
286+
stopRotation(textDisplay)
287+
return@timer
288+
}
289+
290+
// Skip first tick (delay initial transformation by one tick)
291+
if (isFirstUpdate) {
292+
isFirstUpdate = false
293+
return@timer
294+
}
295+
296+
// Update rotation angle
297+
textDisplay.currentRotationAngle += radiansPerTick
298+
299+
// Create quaternion for Y-axis rotation
300+
// Quaternion for rotation around Y-axis: (0, sin(angle/2), 0, cos(angle/2))
301+
val halfAngle = textDisplay.currentRotationAngle / 2f
302+
val quaternionY = sin(halfAngle)
303+
val quaternionW = cos(halfAngle)
304+
305+
// Update left rotation (this rotates the display around Y-axis)
306+
textDisplay.leftRotation = Quaternion4f(0f, quaternionY, 0f, quaternionW)
307+
308+
// Update transformation with new rotation
309+
updateTransformation(textDisplay)
310+
}
311+
}
312+
313+
/**
314+
* Stops the continuous rotation for a Text Display.
315+
*
316+
* @param textDisplay The TextDisplay to stop rotating
317+
*/
318+
fun stopRotation(textDisplay: TextDisplay) {
319+
textDisplay.stopRotation()
320+
}
321+
225322
/**
226323
* Updates the position of an existing Text Display.
227324
*
@@ -246,6 +343,9 @@ class TextDisplayManager {
246343
* @param textDisplay The TextDisplay to remove
247344
*/
248345
fun removeTextDisplay(textDisplay: TextDisplay) {
346+
// Stop any rotation task
347+
stopRotation(textDisplay)
348+
249349
activeDisplays.remove(textDisplay.entityId)
250350

251351
for (viewer in textDisplay.viewers.toSet()) {
@@ -484,15 +584,24 @@ class TextDisplayManager {
484584
val opacity: Int = -1,
485585
val displayFlags: List<TextDisplayFlags>,
486586
val backgroundColor: Int = 0x00000000,
487-
val scale: Vector3f = Vector3f(1f, 1f, 1f),
488-
val viewRange: Float = 1f,
489-
val translation: Vector3f = Vector3f(0f, 0f, 0f),
490-
val leftRotation: Quaternion4f = Quaternion4f(0f, 0f, 0f, 1f),
491-
val rightRotation: Quaternion4f = Quaternion4f(0f, 0f, 0f, 1f),
492-
val interpolationDelay: Int = 0,
493-
val transformationInterpolationDuration: Int = 0,
494-
val positionRotationInterpolationDuration: Int = 0,
587+
var scale: Vector3f = Vector3f(1f, 1f, 1f),
588+
var viewRange: Float = 1f,
589+
var translation: Vector3f = Vector3f(0f, 0f, 0f),
590+
var leftRotation: Quaternion4f = Quaternion4f(0f, 0f, 0f, 1f),
591+
var rightRotation: Quaternion4f = Quaternion4f(0f, 0f, 0f, 1f),
592+
var interpolationDelay: Int = 0,
593+
var transformationInterpolationDuration: Int = 0,
594+
var positionRotationInterpolationDuration: Int = 0,
495595
) {
596+
/**
597+
* Task for continuous rotation (if active).
598+
*/
599+
internal var rotationTask: BukkitTask? = null
600+
601+
/**
602+
* Current rotation angle in radians for continuous rotation.
603+
*/
604+
internal var currentRotationAngle: Float = 0f
496605
/**
497606
* Vertical offset from the base location.
498607
* How far above the ground/position the display should appear.
@@ -583,6 +692,40 @@ class TextDisplayManager {
583692
fun isVisibleTo(player: Player): Boolean {
584693
return player in viewers
585694
}
695+
696+
/**
697+
* Starts continuous rotation around the Y-axis.
698+
* Shortcut for [TextDisplayManager.startRotation].
699+
*
700+
* @param manager The manager instance to use
701+
* @param durationTicks The duration of half a revolution in ticks (default: 100 ticks = 5 seconds)
702+
* @param scale The scale multiplier for the display during rotation (default: 1.0 = no scaling)
703+
*/
704+
fun startRotation(
705+
manager: TextDisplayManager,
706+
durationTicks: Int = 100,
707+
scale: Float = 1.0f
708+
) {
709+
manager.startRotation(this, durationTicks, scale)
710+
}
711+
712+
/**
713+
* Stops the continuous rotation.
714+
* Shortcut for [TextDisplayManager.stopRotation].
715+
*
716+
* @param manager The manager instance to use
717+
*/
718+
fun stopRotation(manager: TextDisplayManager) {
719+
manager.stopRotation(this)
720+
}
721+
722+
/**
723+
* Stops any active rotation task (internal use).
724+
*/
725+
internal fun stopRotation() {
726+
rotationTask?.cancel()
727+
rotationTask = null
728+
}
586729
}
587730
}
588731

0 commit comments

Comments
 (0)