Repository: LuaRocket/rocket
Pull request #41: Add vectors and math functions
Author: login: zNotChill
Created: 2025-05-09T16:33:15Z | Merged: 2025-05-11T14:16:47Z

This PR aims to add 3d vectors with a handful of useful utility functions, taking use of Twine's new overloading feature.

Unfortunately, due to the fact that Lua has a built-in math library, this *does* override it. Which is why I have tried to re-implement the expected behaviour from existing math functions.

As far as I know, this re-implements *nearly* all of the existing math functions. The only current issue is min and max functions not working, due to Twine's lack of a working vararg system. Hopefully, this will be addressed and fixed by me or somebody else if needed.

The reason for this custom math library is that vector utilities would be very useful, such as `math.abs(Double)` and `math.abs(Vector3)`

This PR could be merged, since any existing issues would be addressed and fixed later on, and I think its important for vectors to exist in the main branch before any world/entity stuff is started.


Diff:
diff --git a/build.gradle.kts b/build.gradle.kts
index 6d60124..8327c06 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -44,7 +44,7 @@ dependencies {
     implementation("org.luaj:luaj-jse:3.0.1")
     implementation("net.luckperms:api:5.4")
     implementation("org.jetbrains.kotlin:kotlin-reflect")
-    implementation("dev.znci:twine:v1.0.4")
+    implementation("dev.znci:twine:1.0.7")
     implementation("org.reflections:reflections:0.10.2")
 
     testImplementation(kotlin("test"))
diff --git a/src/main/kotlin/dev/znci/rocket/scripting/globals/tables/Math.kt b/src/main/kotlin/dev/znci/rocket/scripting/globals/tables/Math.kt
new file mode 100644
index 0000000..70254eb
--- /dev/null
+++ b/src/main/kotlin/dev/znci/rocket/scripting/globals/tables/Math.kt
@@ -0,0 +1,171 @@
+package dev.znci.rocket.scripting.globals.tables
+
+import dev.znci.rocket.scripting.annotations.Global
+import dev.znci.rocket.scripting.api.RocketError
+import dev.znci.twine.TwineNative
+import dev.znci.twine.annotations.TwineNativeFunction
+import dev.znci.twine.annotations.TwineOverload
+import kotlin.math.roundToInt
+
+@Global
+class LuaMath : TwineNative("math") {
+    @TwineNativeFunction
+    @TwineOverload
+    fun random(min: Double, max: Double): Double {
+        if (min >= max) {
+            throw RocketError("min value should be less than max value")
+        }
+
+        return (min + (max - min) * Math.random())
+    }
+
+    @TwineNativeFunction
+    @TwineOverload
+    fun random(): Double {
+        return random(0.0, 1.0)
+    }
+
+    @TwineNativeFunction
+    @TwineOverload
+    fun randomInt(min: Double, max: Double): Int {
+        if (min >= max) {
+            throw RocketError("min value should be less than max value")
+        }
+
+        return (min + (max - min) * Math.random()).roundToInt()
+    }
+
+    @TwineNativeFunction
+    @TwineOverload
+    fun randomInt(): Int {
+        return randomInt(0.0, 1.0)
+    }
+
+    @TwineNativeFunction
+    fun cos(x: Double): Double {
+        return kotlin.math.cos(x)
+    }
+
+    @TwineNativeFunction
+    fun sin(x: Double): Double {
+        return kotlin.math.sin(x)
+    }
+
+    @TwineNativeFunction
+    fun tan(x: Double): Double {
+        return kotlin.math.tan(x)
+    }
+
+    @TwineNativeFunction
+    fun acos(x: Double): Double {
+        return kotlin.math.acos(x)
+    }
+
+    @TwineNativeFunction
+    fun asin(x: Double): Double {
+        return kotlin.math.asin(x)
+    }
+
+    @TwineNativeFunction
+    fun atan(x: Double): Double {
+        return kotlin.math.atan(x)
+    }
+
+    @TwineNativeFunction
+    fun atan2(y: Double, x: Double): Double {
+        return kotlin.math.atan2(y, x)
+    }
+
+    @TwineNativeFunction
+    fun sqrt(x: Double): Double {
+        return kotlin.math.sqrt(x)
+    }
+
+    @TwineNativeFunction
+    fun ceil(x: Double): Double {
+        return kotlin.math.ceil(x)
+    }
+
+    @TwineNativeFunction
+    @TwineOverload
+    fun floor(x: Double): Double {
+        return kotlin.math.floor(x)
+    }
+
+    @TwineNativeFunction
+    @TwineOverload
+    fun floor(vector3: LuaVector3): LuaVector3 {
+        return LuaVector3(
+            kotlin.math.floor(vector3.xProperty),
+            kotlin.math.floor(vector3.yProperty),
+            kotlin.math.floor(vector3.zProperty)
+        )
+    }
+
+    @TwineNativeFunction
+    @TwineOverload
+    fun abs(x: Double): Double {
+        return kotlin.math.abs(x)
+    }
+
+    @TwineNativeFunction
+    @TwineOverload
+    fun abs(vector3: LuaVector3): LuaVector3 {
+        return LuaVector3(
+            kotlin.math.abs(vector3.xProperty),
+            kotlin.math.abs(vector3.yProperty),
+            kotlin.math.abs(vector3.zProperty)
+        )
+    }
+
+    @TwineNativeFunction
+    @TwineOverload
+    fun max(vararg values: Double): Double {
+        return values.maxOrNull() ?: 0.0
+    }
+
+    @TwineNativeFunction
+    @TwineOverload
+    fun max(vector3: LuaVector3): Double {
+        return kotlin.math.max(
+            kotlin.math.max(vector3.xProperty, vector3.yProperty),
+            vector3.zProperty
+        )
+    }
+
+    @TwineNativeFunction
+    @TwineOverload
+    fun min(vararg values: Double): Double {
+        return values.minOrNull() ?: 0.0
+    }
+
+    @TwineNativeFunction
+    @TwineOverload
+    fun min(vector3: LuaVector3): Double {
+        return kotlin.math.min(
+            kotlin.math.min(vector3.xProperty, vector3.yProperty),
+            vector3.zProperty
+        )
+    }
+
+    @TwineNativeFunction
+    @TwineOverload
+    fun round(x: Double): Double {
+        return kotlin.math.round(x)
+    }
+
+    @TwineNativeFunction
+    @TwineOverload
+    fun round(vector3: LuaVector3): LuaVector3 {
+        return LuaVector3(
+            kotlin.math.round(vector3.xProperty),
+            kotlin.math.round(vector3.yProperty),
+            kotlin.math.round(vector3.zProperty)
+        )
+    }
+
+    @TwineNativeFunction
+    fun toDegrees(radians: Double): Double {
+        return Math.toDegrees(radians)
+    }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/dev/znci/rocket/scripting/globals/tables/Vector3.kt b/src/main/kotlin/dev/znci/rocket/scripting/globals/tables/Vector3.kt
new file mode 100644
index 0000000..e42bf2b
--- /dev/null
+++ b/src/main/kotlin/dev/znci/rocket/scripting/globals/tables/Vector3.kt
@@ -0,0 +1,234 @@
+package dev.znci.rocket.scripting.globals.tables
+
+import dev.znci.rocket.scripting.annotations.Global
+import dev.znci.twine.TwineNative
+import dev.znci.twine.annotations.TwineNativeFunction
+import dev.znci.twine.annotations.TwineNativeProperty
+import dev.znci.twine.annotations.TwineOverload
+import org.bukkit.util.Vector
+
+@Global
+class LuaVectors3 : TwineNative("vector3") {
+    @TwineNativeFunction
+    fun new(x: Double, y: Double, z: Double): LuaVector3 {
+        return LuaVector3(x, y, z)
+    }
+}
+
+class LuaVector3(
+    x: Double,
+    y: Double,
+    z: Double
+) : TwineNative("") {
+    private var bukkitVector = Vector(x, y, z)
+
+    @TwineNativeFunction
+    @TwineOverload
+    fun add(vector3: LuaVector3): LuaVector3 {
+        bukkitVector.add(vector3.bukkitVector)
+        return this
+    }
+
+    @TwineNativeFunction
+    @TwineOverload
+    fun add(x: Double, y: Double, z: Double): LuaVector3 {
+        bukkitVector.add(Vector(x, y, z))
+        return this
+    }
+
+    @TwineNativeFunction
+    @TwineOverload
+    fun set(vector3: LuaVector3): LuaVector3 {
+        bukkitVector = vector3.bukkitVector
+        return this
+    }
+
+    @TwineNativeFunction
+    @TwineOverload
+    fun set(x: Double, y: Double, z: Double): LuaVector3 {
+        bukkitVector = Vector(x, y, z)
+        return this
+    }
+
+    @TwineNativeFunction
+    fun clone(): LuaVector3 {
+        return bukkitVector.toLuaVector3()
+    }
+
+    @TwineNativeFunction
+    fun crossProduct(comparedVector: LuaVector3): LuaVector3 {
+        // TODO: Evaluate whether getCrossProduct or crossProduct is better here
+        return bukkitVector.getCrossProduct(
+            comparedVector.bukkitVector
+        ).toLuaVector3()
+    }
+
+    @TwineNativeFunction
+    fun cross(comparedVector: LuaVector3): LuaVector3 {
+        return crossProduct(comparedVector)
+    }
+
+    @TwineNativeFunction
+    fun distance(comparedVector: LuaVector3): Double {
+        return bukkitVector.distance(
+            comparedVector.bukkitVector
+        )
+    }
+
+    @TwineNativeFunction
+    @TwineOverload
+    fun divide(comparedVector: LuaVector3): LuaVector3 {
+        return bukkitVector.divide(
+            comparedVector.bukkitVector
+        ).toLuaVector3()
+    }
+
+    @TwineNativeFunction
+    @TwineOverload
+    fun divide(x: Double, y: Double, z: Double): LuaVector3 {
+        return bukkitVector.divide(Vector(x, y, z)).toLuaVector3()
+    }
+
+    @TwineNativeFunction
+    @TwineOverload
+    fun div(comparedVector: LuaVector3): LuaVector3 {
+        return divide(comparedVector)
+    }
+
+    @TwineNativeFunction
+    @TwineOverload
+    fun div(x: Double, y: Double, z: Double): LuaVector3 {
+        return divide(x, y, z)
+    }
+
+    @TwineNativeFunction
+    @TwineOverload
+    fun multiply(comparedVector: LuaVector3): LuaVector3 {
+        return bukkitVector.multiply(
+            comparedVector.bukkitVector
+        ).toLuaVector3()
+    }
+
+    @TwineNativeFunction
+    @TwineOverload
+    fun multiply(x: Double, y: Double, z: Double): LuaVector3 {
+        return bukkitVector.multiply(Vector(x, y, z)).toLuaVector3()
+    }
+
+    @TwineNativeFunction
+    @TwineOverload
+    fun mul(comparedVector: LuaVector3): LuaVector3 {
+        return multiply(comparedVector)
+    }
+
+    @TwineNativeFunction
+    @TwineOverload
+    fun mul(x: Double, y: Double, z: Double): LuaVector3 {
+        return multiply(x, y, z)
+    }
+
+    @TwineNativeFunction
+    @TwineOverload
+    fun subtract(comparedVector: LuaVector3): LuaVector3 {
+        return bukkitVector.subtract(
+            comparedVector.bukkitVector
+        ).toLuaVector3()
+    }
+
+    @TwineNativeFunction
+    @TwineOverload
+    fun subtract(x: Double, y: Double, z: Double): LuaVector3 {
+        return bukkitVector.subtract(Vector(x, y, z)).toLuaVector3()
+    }
+
+    @TwineNativeFunction
+    @TwineOverload
+    fun sub(comparedVector: LuaVector3): LuaVector3 {
+        return subtract(comparedVector)
+    }
+
+    @TwineNativeFunction
+    @TwineOverload
+    fun sub(x: Double, y: Double, z: Double): LuaVector3 {
+        return subtract(x, y, z)
+    }
+
+    @TwineNativeFunction
+    fun dotProduct(comparedVector: LuaVector3): Double {
+        return bukkitVector.dot(
+            comparedVector.bukkitVector
+        )
+    }
+
+    @TwineNativeFunction
+    fun dot(comparedVector: LuaVector3): Double {
+        return dotProduct(comparedVector)
+    }
+
+    @TwineNativeFunction
+    fun midpoint(comparedVector: LuaVector3): LuaVector3 {
+        return bukkitVector.getMidpoint(
+            comparedVector.bukkitVector
+        ).toLuaVector3()
+    }
+
+    @TwineNativeFunction
+    fun inAABB(min: LuaVector3, max: LuaVector3): Boolean {
+        return bukkitVector.isInAABB(min.bukkitVector, max.bukkitVector)
+    }
+
+    @TwineNativeFunction
+    fun inSphere(center: LuaVector3, radius: Double): Boolean {
+        return bukkitVector.isInSphere(center.bukkitVector, radius)
+    }
+
+    @TwineNativeFunction
+    fun rotateAround(comparedVector: LuaVector3, angle: Double): LuaVector3 {
+        return this.bukkitVector.rotateAroundAxis(
+            comparedVector.bukkitVector, angle
+        ).toLuaVector3()
+    }
+
+    @TwineNativeFunction
+    fun normalize(): LuaVector3 {
+        return bukkitVector.normalize().toLuaVector3()
+    }
+
+    @TwineNativeProperty("magnitude")
+    val magnitude: Double
+        get() = bukkitVector.length()
+
+    fun Vector.toLuaVector3(): LuaVector3 {
+        return LuaVector3(
+            this.x,
+            this.y,
+            this.z
+        )
+    }
+
+    @TwineNativeProperty("x")
+    var xProperty: Double
+        get() = bukkitVector.x
+        set(value) {
+            bukkitVector.x = value
+        }
+
+    @TwineNativeProperty("y")
+    var yProperty: Double
+        get() = bukkitVector.y
+        set(value) {
+            bukkitVector.y = value
+        }
+
+    @TwineNativeProperty("z")
+    var zProperty: Double
+        get() = bukkitVector.z
+        set(value) {
+            bukkitVector.z = value
+        }
+
+    @TwineNativeFunction
+    override fun toString(): String {
+        return "vector3($xProperty, $yProperty, $zProperty)"
+    }
+}
\ No newline at end of file
diff --git a/src/test/kotlin/dev/znci/rocket/scripting/globals/tables/LuaCommandsTest.kt b/src/test/kotlin/dev/znci/rocket/scripting/globals/tables/LuaCommandsTest.kt
index d04280b..5a907d5 100644
--- a/src/test/kotlin/dev/znci/rocket/scripting/globals/tables/LuaCommandsTest.kt
+++ b/src/test/kotlin/dev/znci/rocket/scripting/globals/tables/LuaCommandsTest.kt
@@ -14,7 +14,6 @@ import org.mockbukkit.mockbukkit.ServerMock
  * a new instance of `LuaCommand`.
  */
 class LuaCommandsTest {
-
     private lateinit var mockServer: ServerMock
     private lateinit var plugin: Rocket
 
diff --git a/src/test/kotlin/dev/znci/rocket/scripting/globals/tables/LuaLocationTest.kt b/src/test/kotlin/dev/znci/rocket/scripting/globals/tables/LuaLocationTest.kt
new file mode 100644
index 0000000..4757c8c
--- /dev/null
+++ b/src/test/kotlin/dev/znci/rocket/scripting/globals/tables/LuaLocationTest.kt
@@ -0,0 +1,43 @@
+package dev.znci.rocket.scripting.globals.tables
+
+import dev.znci.rocket.Rocket
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.luaj.vm2.Globals
+import org.luaj.vm2.lib.jse.JsePlatform
+import org.mockbukkit.mockbukkit.MockBukkit
+import org.mockbukkit.mockbukkit.ServerMock
+import kotlin.test.assertEquals
+
+class LuaLocationTest {
+    private lateinit var mockServer: ServerMock
+    private lateinit var plugin: Rocket
+
+    @BeforeEach
+    fun setUp() {
+        mockServer = MockBukkit.mock()
+        plugin = MockBukkit.load(Rocket::class.java)
+        mockServer.worlds.add(mockServer.addSimpleWorld("world"))
+    }
+
+    fun run(script: String): Any {
+        val globals: Globals = JsePlatform.standardGlobals()
+        globals.set("location", LuaLocations().table)
+
+        val newScript = """
+            local loc = location.new(1, 2, 3, "world", 0, 0)
+            $script
+        """.trimIndent()
+
+        val result = globals.load(newScript, "test.lua").call()
+
+        return result
+    }
+
+    @Test
+    fun `new should create a new LuaLocation instance`() {
+        val result = run("return loc.x")
+
+        assertEquals(1.0, result.toString().toDouble())
+    }
+}
\ No newline at end of file
diff --git a/src/test/kotlin/dev/znci/rocket/scripting/globals/tables/LuaMathTest.kt b/src/test/kotlin/dev/znci/rocket/scripting/globals/tables/LuaMathTest.kt
new file mode 100644
index 0000000..d7a92fb
--- /dev/null
+++ b/src/test/kotlin/dev/znci/rocket/scripting/globals/tables/LuaMathTest.kt
@@ -0,0 +1,141 @@
+package dev.znci.rocket.scripting.globals.tables
+
+import org.junit.jupiter.api.Test
+import org.luaj.vm2.Globals
+import org.luaj.vm2.LuaTable
+import org.luaj.vm2.lib.jse.JsePlatform
+import kotlin.test.assertEquals
+
+class LuaMathTest {
+    fun run(script: String): Any {
+        val globals: Globals = JsePlatform.standardGlobals()
+        globals.set("vector3", LuaVectors3().table)
+        globals.set("math", LuaMath().table)
+
+        val result = globals.load(script, "test.lua").call()
+
+        return result
+    }
+
+    @Test
+    fun `acos should return the correct value`() {
+        val result = run("return math.acos(0.5)")
+
+        assertEquals(1.0471976, result.toString().toDouble())
+    }
+
+    @Test
+    fun `asin should return the correct value`() {
+        val result = run("return math.asin(0.5)")
+
+        assertEquals(0.5235988, result.toString().toDouble())
+    }
+
+    @Test
+    fun `atan should return the correct value`() {
+        val result = run("return math.atan(1)")
+
+        assertEquals(0.7853982, result.toString().toDouble())
+    }
+
+    @Test
+    fun `atan2 should return the correct value`() {
+        val result = run("return math.atan2(1, 1)")
+
+        assertEquals(0.7853982, result.toString().toDouble())
+    }
+
+    @Test
+    fun `random should return a value between 0 and 1`() {
+        val result = run("return math.random()")
+
+        assert(result.toString().toDouble() in 0.0..1.0)
+    }
+
+    @Test
+    fun `random(1, 100) should return a value between 1 and 100`() {
+        val result = run("return math.random(1, 100)")
+
+        assert(result.toString().toDouble() in 1.0..100.0)
+    }
+
+    @Test
+    fun `randomInt should return a value between 1 and 100`() {
+        val result = run("return math.randomInt(1, 100)")
+
+        assert(result.toString().toDouble() in 1.0..100.0)
+    }
+
+    @Test
+    fun `randomInt should return a value between 0 and 1`() {
+        val result = run("return math.randomInt()")
+
+        assert(result.toString().toDouble() in 0.0..1.0)
+    }
+
+    @Test
+    fun `cos should return the correct value`() {
+        val result = run("return math.cos(0)")
+
+        assertEquals(1.0, result.toString().toDouble())
+    }
+
+    @Test
+    fun `sin should return the correct value`() {
+        val result = run("return math.sin(0)")
+
+        assertEquals(0.0, result.toString().toDouble())
+    }
+
+    @Test
+    fun `tan should return the correct value`() {
+        val result = run("return math.tan(0)")
+
+        assertEquals(0.0, result.toString().toDouble())
+    }
+
+    @Test
+    fun `min should return the correct value`() {
+        val result = run("return math.min(1, 2)")
+
+        assertEquals(1.0, result.toString().toDouble())
+    }
+
+    @Test
+    fun `min with a vector should return the correct value`() {
+        val result = run("return math.min(vector3.new(1, 2, 3))")
+
+        assertEquals(1.0, result.toString().toDouble())
+    }
+
+    @Test
+    fun `max should return the correct value`() {
+        val result = run("return math.max(1, 2)")
+
+        assertEquals(2.0, result.toString().toDouble())
+    }
+
+    @Test
+    fun `max with a vector should return the correct value`() {
+        val result = run("return math.max(vector3.new(1, 2, 3))")
+
+        assertEquals(3.0, result.toString().toDouble())
+    }
+
+    @Test
+    fun `abs should return the correct value`() {
+        val result = run("return math.abs(-1)")
+
+        assertEquals(1.0, result.toString().toDouble())
+    }
+
+    @Test
+    fun `abs with a vector should return the correct value`() {
+        val result = run("return math.abs(vector3.new(-1, -2, -3))")
+        val table = result as LuaTable
+
+        assertEquals(1.0, table.get("x").todouble())
+        assertEquals(2.0, table.get("y").todouble())
+        assertEquals(3.0, table.get("z").todouble())
+    }
+}
\ No newline at end of file
diff --git a/src/test/kotlin/dev/znci/rocket/scripting/globals/tables/LuaVector3Test.kt b/src/test/kotlin/dev/znci/rocket/scripting/globals/tables/LuaVector3Test.kt
new file mode 100644
index 0000000..5684688
--- /dev/null
+++ b/src/test/kotlin/dev/znci/rocket/scripting/globals/tables/LuaVector3Test.kt
@@ -0,0 +1,369 @@
+package dev.znci.rocket.scripting.globals.tables
+
+import org.junit.jupiter.api.Test
+import org.luaj.vm2.Globals
+import org.luaj.vm2.LuaTable
+import org.luaj.vm2.lib.jse.JsePlatform
+import kotlin.test.assertEquals
+
+class LuaVector3Test {
+    fun run(script: String): Any {
+        val globals: Globals = JsePlatform.standardGlobals()
+        globals.set("vector3", LuaVectors3().table)
+
+        val newScript = """
+            local vector = vector3.new(1, 2, 3)
+            local vector2 = vector3.new(4, 5, 6)
+            $script
+        """.trimIndent()
+
+        val result = globals.load(newScript, "test.lua").call()
+
+        return result
+    }
+
+    @Test
+    fun `vector3 addition should return the correct value`() {
+        val result = run("""
+            vector.add(vector3.new(1, 2, 3))
+            return vector
+        """.trimIndent())
+        val table = result as LuaTable
+
+        assertEquals(2.0, table.get("x").todouble())
+        assertEquals(4.0, table.get("y").todouble())
+        assertEquals(6.0, table.get("z").todouble())
+    }
+
+    @Test
+    fun `vector3 set should return the correct value`() {
+        val result = run("""
+            vector.set(vector3.new(5, 6, 7))
+            return vector
+        """.trimIndent())
+        val table = result as LuaTable
+
+        assertEquals(5.0, table.get("x").todouble())
+        assertEquals(6.0, table.get("y").todouble())
+        assertEquals(7.0, table.get("z").todouble())
+    }
+
+    @Test
+    fun `vector3 crossProduct should return the correct value`() {
+        val result = run("""
+            local crossProduct = vector.crossProduct(vector2)
+            return crossProduct
+        """.trimIndent())
+        val table = result as LuaTable
+
+        assertEquals(-3.0, table.get("x").todouble())
+        assertEquals(6.0, table.get("y").todouble())
+        assertEquals(-3.0, table.get("z").todouble())
+    }
+
+    @Test
+    fun `vector3 cross should return the correct value`() {
+        val result = run("""
+            local crossProduct = vector.cross(vector2)
+            return crossProduct
+        """.trimIndent())
+        val table = result as LuaTable
+
+        assertEquals(-3.0, table.get("x").todouble())
+        assertEquals(6.0, table.get("y").todouble())
+        assertEquals(-3.0, table.get("z").todouble())
+    }
+
+    @Test
+    fun `vector3 clone should return the correct value`() {
+        val result = run("""
+            local clone = vector.clone()
+            return clone
+        """.trimIndent())
+        val table = result as LuaTable
+
+        assertEquals(1.0, table.get("x").todouble())
+        assertEquals(2.0, table.get("y").todouble())
+        assertEquals(3.0, table.get("z").todouble())
+    }
+
+    @Test
+    fun `vector3 distance should return the correct value`() {
+        val result = run("""
+            local distance = vector.distance(vector2)
+            return distance
+        """.trimIndent())
+
+        assertEquals(5.196152, result.toString().toDouble())
+    }
+
+    @Test
+    fun `vector3 divide should return the correct value`() {
+        val result = run("""
+            local divided = vector.divide(vector2)
+            return divided
+        """.trimIndent())
+        val table = result as LuaTable
+
+        assertEquals(0.25, table.get("x").todouble())
+        assertEquals(0.4, table.get("y").todouble())
+        assertEquals(0.5, table.get("z").todouble())
+    }
+
+    @Test
+    fun `vector3 div should return the correct value`() {
+        val result = run("""
+            local divided = vector.div(vector2)
+            return divided
+        """.trimIndent())
+        val table = result as LuaTable
+
+        assertEquals(0.25, table.get("x").todouble())
+        assertEquals(0.4, table.get("y").todouble())
+        assertEquals(0.5, table.get("z").todouble())
+    }
+
+    @Test
+    fun `vector3 multiply should return the correct value`() {
+        val result = run("""
+            local multiplied = vector.multiply(vector2)
+            return multiplied
+        """.trimIndent())
+        val table = result as LuaTable
+
+        assertEquals(4.0, table.get("x").todouble())
+        assertEquals(10.0, table.get("y").todouble())
+        assertEquals(18.0, table.get("z").todouble())
+    }
+
+    @Test
+    fun `vector3 multiply using non-vector parameters should return the correct value`() {
+        val result = run("""
+            local multiplied = vector.multiply(4, 5, 6)
+            return multiplied
+        """.trimIndent())
+        val table = result as LuaTable
+
+        assertEquals(4.0, table.get("x").todouble())
+        assertEquals(10.0, table.get("y").todouble())
+        assertEquals(18.0, table.get("z").todouble())
+    }
+
+    @Test
+    fun `vector3 mul should return the correct value`() {
+        val result = run("""
+            local multiplied = vector.mul(vector2)
+            return multiplied
+        """.trimIndent())
+        val table = result as LuaTable
+
+        assertEquals(4.0, table.get("x").todouble())
+        assertEquals(10.0, table.get("y").todouble())
+        assertEquals(18.0, table.get("z").todouble())
+    }
+
+    @Test
+    fun `vector3 mul using non-vector parameters should return the correct value`() {
+        val result = run("""
+            local multiplied = vector.mul(4, 5, 6)
+            return multiplied
+        """.trimIndent())
+        val table = result as LuaTable
+
+        assertEquals(4.0, table.get("x").todouble())
+        assertEquals(10.0, table.get("y").todouble())
+        assertEquals(18.0, table.get("z").todouble())
+    }
+
+    @Test
+    fun `vector3 subtract should return the correct value`() {
+        val result = run("""
+            local subtracted = vector.subtract(vector2)
+            return subtracted
+        """.trimIndent())
+        val table = result as LuaTable
+
+        assertEquals(-3.0, table.get("x").todouble())
+        assertEquals(-3.0, table.get("y").todouble())
+        assertEquals(-3.0, table.get("z").todouble())
+    }
+
+    @Test
+    fun `vector3 subtract using non-vector parameters should return the correct value`() {
+        val result = run("""
+            local subtracted = vector.subtract(4, 5, 6)
+            return subtracted
+        """.trimIndent())
+        val table = result as LuaTable
+
+        assertEquals(-3.0, table.get("x").todouble())
+        assertEquals(-3.0, table.get("y").todouble())
+        assertEquals(-3.0, table.get("z").todouble())
+    }
+
+    @Test
+    fun `vector3 sub should return the correct value`() {
+        val result = run("""
+            local subtracted = vector.sub(vector2)
+            return subtracted
+        """.trimIndent())
+        val table = result as LuaTable
+
+        assertEquals(-3.0, table.get("x").todouble())
+        assertEquals(-3.0, table.get("y").todouble())
+        assertEquals(-3.0, table.get("z").todouble())
+    }
+
+    @Test
+    fun `vector3 sub using non-vector parameters should return the correct value`() {
+        val result = run("""
+            local subtracted = vector.sub(4, 5, 6)
+            return subtracted
+        """.trimIndent())
+        val table = result as LuaTable
+
+        assertEquals(-3.0, table.get("x").todouble())
+        assertEquals(-3.0, table.get("y").todouble())
+        assertEquals(-3.0, table.get("z").todouble())
+    }
+
+    @Test
+    fun `vector3 dotProduct should return the correct value`() {
+        val result = run("""
+            local dotProduct = vector.dotProduct(vector2)
+            return dotProduct
+        """.trimIndent())
+
+        assertEquals(32.0, result.toString().toDouble())
+    }
+
+    @Test
+    fun `vector3 dot should return the correct value`() {
+        val result = run("""
+            local dotProduct = vector.dot(vector2)
+            return dotProduct
+        """.trimIndent())
+
+        assertEquals(32.0, result.toString().toDouble())
+    }
+
+    @Test
+    fun `vector3 midpoint should return the correct value`() {
+        val result = run("""
+            local midpoint = vector.midpoint(vector2)
+            return midpoint
+        """.trimIndent())
+        val table = result as LuaTable
+
+        assertEquals(2.5, table.get("x").todouble())
+        assertEquals(3.5, table.get("y").todouble())
+        assertEquals(4.5, table.get("z").todouble())
+    }
+
+    @Test
+    fun `vector3 inAABB should return the correct value`() {
+        val result = run("""
+            local inAABB = vector.inAABB(vector3.new(0, 0, 0), vector3.new(5, 6, 7))
+            return inAABB
+        """.trimIndent())
+
+        assertEquals(true, result.toString().toBoolean())
+    }
+
+    @Test
+    fun `vector3 inAABB with invalid coordinates should return the correct value`() {
+        val result = run("""
+            local inAABB = vector.inAABB(vector3.new(0, 0, 0), vector3.new(1, 1, 1))
+            return inAABB
+        """.trimIndent())
+
+        assertEquals(false, result.toString().toBoolean())
+    }
+
+    @Test
+    fun `vector3 inSphere should return the correct value`() {
+        val result = run("""
+            local inSphere = vector.inSphere(vector3.new(0, 0, 0), 5)
+            return inSphere
+        """.trimIndent())
+
+        assertEquals(true, result.toString().toBoolean())
+    }
+
+    @Test
+    fun `vector3 inSphere with invalid coordinates should return the correct value`() {
+        val result = run("""
+            local inSphere = vector.inSphere(vector3.new(0, 0, 0), 1)
+            return inSphere
+        """.trimIndent())
+
+        assertEquals(false, result.toString().toBoolean())
+    }
+
+    @Test
+    fun `vector3 rotateAround should return the correct value`() {
+        val result = run("""
+            local rotated = vector.rotateAround(vector3.new(10, 10, 10), 90)
+            return rotated
+        """.trimIndent())
+        val table = result as LuaTable
+
+        assertEquals(2.9642228305135787, table.get("x").todouble())
+        assertEquals(0.967701571231181, table.get("y").todouble())
+        assertEquals(2.0680755982552386, table.get("z").todouble())
+    }
+
+    @Test
+    fun `vector3 normalize should return the correct value`() {
+        val result = run("""
+            local normalized = vector.normalize()
+            return normalized
+        """.trimIndent())
+        val table = result as LuaTable
+
+        assertEquals(0.2672612419124244, table.get("x").todouble())
+        assertEquals(0.5345224838248488, table.get("y").todouble())
+        assertEquals(0.8017837257372732, table.get("z").todouble())
+    }
+
+    @Test
+    fun `vector3 magnitude should return the correct value`() {
+        val result = run("""
+            local magnitude = vector.magnitude
+            return magnitude
+        """.trimIndent())
+
+        assertEquals(3.7416575, result.toString().toDouble())
+    }
+
+    @Test
+    fun `vector3 toString should return the correct value`() {
+        val result = run("""
+            local str = vector.toString()
+            return str
+        """.trimIndent())
+
+        assertEquals("vector3(1.0, 2.0, 3.0)", result.toString())
+    }
+
+    @Test
+    fun `vector3 x property should return the correct value`() {
+        val result = run("return vector.x")
+
+        assertEquals(1.0, result.toString().toDouble())
+    }
+
+    @Test
+    fun `vector3 y property should return the correct value`() {
+        val result = run("return vector.y")
+
+        assertEquals(2.0, result.toString().toDouble())
+    }
+
+    @Test
+    fun `vector3 z property should return the correct value`() {
+        val result = run("return vector.z")
+
+        assertEquals(3.0, result.toString().toDouble())
+    }
+}
\ No newline at end of file


Reviews:
user: login: gibbiemonster
body: LGTM, but remember to bump twine version.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-10T23:32:55Z
user: login: gibbiemonster
body: Nevermind. Write unit tests :slightly_smiling_face: 
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-10T23:33:11Z
user: login: gibbiemonster
body: Just some test style things. Otherwise, they pass and run.
Well done :slightly_smiling_face: 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-11T14:16:25Z

Review comments:
diff_hunk: @@ -44,7 +44,7 @@ dependencies {
     implementation("org.luaj:luaj-jse:3.0.1")
     implementation("net.luckperms:api:5.4")
     implementation("org.jetbrains.kotlin:kotlin-reflect")
-    implementation("dev.znci:twine:v1.0.4")
+    implementation("dev.znci:twine:1.0.6")
path: build.gradle.kts
user: login: gibbiemonster
body: This should be bumped to 1.0.7.
created_at: 2025-05-10T23:31:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,172 @@
+package dev.znci.rocket.scripting.globals.tables
+
+import dev.znci.rocket.scripting.annotations.Global
+import dev.znci.rocket.scripting.api.RocketError
+import dev.znci.twine.TwineNative
+import dev.znci.twine.annotations.TwineNativeFunction
+import dev.znci.twine.annotations.TwineOverload
+import java.lang.Math
path: src/main/kotlin/dev/znci/rocket/scripting/globals/tables/Math.kt
user: login: gibbiemonster
body: Why kotlin.math and java.lang.Math?
created_at: 2025-05-10T23:32:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,43 @@
+package dev.znci.rocket.scripting.globals.tables
+
+import dev.znci.rocket.Rocket
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.luaj.vm2.Globals
+import org.luaj.vm2.lib.jse.JsePlatform
+import org.mockbukkit.mockbukkit.MockBukkit
+import org.mockbukkit.mockbukkit.ServerMock
+import kotlin.test.assertEquals
+
+class LuaLocationTest {
+    private lateinit var mockServer: ServerMock
+    private lateinit var plugin: Rocket
+
+    @BeforeEach
+    fun setUp() {
+        mockServer = MockBukkit.mock()
+        plugin = MockBukkit.load(Rocket::class.java)
+        mockServer.worlds.add(mockServer.addSimpleWorld("world"))
+    }
+
+    fun run(script: String): Any {
+        val globals: Globals = JsePlatform.standardGlobals()
+        globals.set("location", LuaLocations().table)
+
+        val newScript = """
+            local loc = location.new(1, 2, 3, "world", 0, 0)
path: src/test/kotlin/dev/znci/rocket/scripting/globals/tables/LuaLocationTest.kt
user: login: gibbiemonster
body: This should be part of the test case, not the utility function
created_at: 2025-05-11T14:14:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,369 @@
+package dev.znci.rocket.scripting.globals.tables
+
+import org.junit.jupiter.api.Test
+import org.luaj.vm2.Globals
+import org.luaj.vm2.LuaTable
+import org.luaj.vm2.lib.jse.JsePlatform
+import kotlin.test.assertEquals
+
+class LuaVector3Test {
+    fun run(script: String): Any {
+        val globals: Globals = JsePlatform.standardGlobals()
+        globals.set("vector3", LuaVectors3().table)
+
+        val newScript = """
+            local vector = vector3.new(1, 2, 3)
path: src/test/kotlin/dev/znci/rocket/scripting/globals/tables/LuaVector3Test.kt
user: login: gibbiemonster
body: Like previously, this should be in the test case, and not the utility function 
created_at: 2025-05-11T14:15:50Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-09T16:40:23Z
body: # [Qodana](https://qodana.cloud/projects/l00xX/reports/rWL0N) Community for JVM

**88 new problems** were found

| Inspection name | Severity | Problems |
| --- | --- | --- |
| `Unused symbol` | 🔶 Warning | 61 |
| `Unused import directive` | 🔶 Warning | 11 |
| `Redundant call of conversion method` | 🔶 Warning | 2 |
| `Unresolved reference in KDoc` | 🔶 Warning | 1 |
| `Usage of redundant or deprecated syntax or deprecated symbols` | 🔶 Warning | 1 |
| `Redundant nullable return type` | 🔶 Warning | 1 |
| `Class member can have 'private' visibility` | ◽️ Notice | 6 |
| `If-Null return/break/... foldable to '?:'` | ◽️ Notice | 2 |
| `Variable declaration could be moved inside 'when'` | ◽️ Notice | 1 |
| `Redundant empty primary constructor` | ◽️ Notice | 1 |
| `Accessor call that can be replaced with property access syntax` | ◽️ Notice | 1 |



☁️ [View the detailed Qodana report](https://qodana.cloud/projects/l00xX/reports/rWL0N)

<details>
<summary>Contact Qodana team</summary>

Contact us at [qodana-support@jetbrains.com](mailto:qodana-support@jetbrains.com)
  - Or via our issue tracker: https://jb.gg/qodana-issue
  - Or share your feedback: https://jb.gg/qodana-discussions
</details>
<!-- JetBrains/qodana-action@v2024.3.4 : Qodana Community for JVM -->
author_association: NONE
user: login: zNotChill
created_at: 2025-05-11T00:40:57Z
body: Reviewing again right now is not necessary, since I am now working on unit tests which I will finish tomorrow.
author_association: CONTRIBUTOR