Files
plainleaf/client/space_lua/stdlib/math.ts
T
Matouš Jan Fialka 11347fee72 Merge on main
Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz>
2026-02-23 07:33:51 +01:00

191 lines
6.2 KiB
TypeScript

import {
LuaBuiltinFunction,
LuaMultiRes,
LuaRuntimeError,
LuaTable,
} from "../runtime.ts";
import { isNegativeZero, isTaggedFloat, makeLuaFloat } from "../numeric.ts";
import { LuaPRNG } from "./prng.ts";
// One PRNG per module load, auto-seeded at startup
const prng = new LuaPRNG();
// Fast unwrap: avoids function call overhead for the common plain-number case
function untagNumber(x: any): number {
if (typeof x === "number") return x;
if (isTaggedFloat(x)) return x.value;
return Number(x);
}
export const mathApi = new LuaTable({
// math constants
huge: 1 / 0,
pi: Math.PI,
// math.type(x) => "integer" | "float" | nil
type: new LuaBuiltinFunction((_sf, x?: any) => {
if (x === undefined) {
throw new LuaRuntimeError(
"bad argument #1 to 'math.type' (value expected)",
_sf,
);
}
if (isTaggedFloat(x)) {
return "float";
}
if (typeof x === "number") {
if (!Number.isFinite(x) || isNegativeZero(x)) {
return "float";
}
return Number.isInteger(x) ? "integer" : "float";
}
if (typeof x === "bigint") {
return "integer";
}
return null;
}),
/**
* If the value x is representable as a Lua integer, returns an integer
* with that value. Otherwise returns nil.
* Strings are NOT accepted — only Lua number values.
*/
tointeger: new LuaBuiltinFunction((_sf, x?: any) => {
if (typeof x === "number") {
return Number.isInteger(x) && isFinite(x) ? x : null;
}
if (isTaggedFloat(x)) {
const n = x.value;
return Number.isInteger(n) && isFinite(n) ? n : null;
}
if (typeof x === "string") {
const n = untagNumber(x); // Number(x) coerces the string
if (isNaN(n) || !isFinite(n) || !Number.isInteger(n)) return null;
return n;
}
return null;
}),
/**
* When called without arguments, returns a pseudo-random float with
* uniform distribution in the range [0,1). When called with two
* integers m and n, math.random returns a pseudo-random integer
* with uniform distribution in the range [m, n]. The call
* math.random(n), for a positive n, is equivalent to
* math.random(1,n). The call math.random(0) produces an integer
* with all bits (pseudo)random.
*/
random: new LuaBuiltinFunction((_sf, m?: number, n?: number) => {
if (m !== undefined) m = untagNumber(m);
if (n !== undefined) n = untagNumber(n);
try {
return prng.random(m, n);
} catch (e: any) {
throw new LuaRuntimeError(e.message, _sf);
}
}),
/**
* Seeds the pseudo-random generator. With no arguments, uses a
* time-based seed. Returns the two seed integers used (Lua 5.4 contract).
*/
randomseed: new LuaBuiltinFunction((_sf, x?: number, y?: number) => {
if (x !== undefined) x = untagNumber(x);
if (y !== undefined) y = untagNumber(y);
const [s1, s2] = prng.randomseed(x, y);
return new LuaMultiRes([s1, s2]);
}),
// Basic functions
abs: new LuaBuiltinFunction((_sf, x: number) => Math.abs(untagNumber(x))),
ceil: new LuaBuiltinFunction((_sf, x: number) => Math.ceil(untagNumber(x))),
floor: new LuaBuiltinFunction((_sf, x: number) => Math.floor(untagNumber(x))),
max: new LuaBuiltinFunction((_sf, ...args: number[]) =>
Math.max(...args.map(untagNumber))
),
min: new LuaBuiltinFunction((_sf, ...args: number[]) =>
Math.min(...args.map(untagNumber))
),
// Rounding and remainder
fmod: new LuaBuiltinFunction((_sf, x: number, y: number) =>
untagNumber(x) % untagNumber(y)
),
modf: new LuaBuiltinFunction((_sf, x: number) => {
const xn = untagNumber(x);
const int = Math.trunc(xn);
// Guarantee that the `frac` part is always Lua float
const frac = makeLuaFloat(xn - int);
return new LuaMultiRes([int, frac]);
}),
// Power and logarithms
exp: new LuaBuiltinFunction((_sf, x: number) => Math.exp(untagNumber(x))),
log: new LuaBuiltinFunction((_sf, x: number, base?: number) => {
if (base === undefined) {
return Math.log(untagNumber(x));
}
return Math.log(untagNumber(x)) / Math.log(untagNumber(base));
}),
pow: new LuaBuiltinFunction((_sf, x: number, y: number) =>
Math.pow(untagNumber(x), untagNumber(y))
),
sqrt: new LuaBuiltinFunction((_sf, x: number) => Math.sqrt(untagNumber(x))),
// Trigonometric functions
cos: new LuaBuiltinFunction((_sf, x: number) => Math.cos(untagNumber(x))),
sin: new LuaBuiltinFunction((_sf, x: number) => Math.sin(untagNumber(x))),
tan: new LuaBuiltinFunction((_sf, x: number) => Math.tan(untagNumber(x))),
acos: new LuaBuiltinFunction((_sf, x: number) => Math.acos(untagNumber(x))),
asin: new LuaBuiltinFunction((_sf, x: number) => Math.asin(untagNumber(x))),
atan: new LuaBuiltinFunction((_sf, y: number, x?: number) => {
if (x === undefined) {
return Math.atan(untagNumber(y));
}
return Math.atan2(untagNumber(y), untagNumber(x));
}),
// Hyperbolic functions
cosh: new LuaBuiltinFunction((_sf, x: number) => Math.cosh(untagNumber(x))),
sinh: new LuaBuiltinFunction((_sf, x: number) => Math.sinh(untagNumber(x))),
tanh: new LuaBuiltinFunction((_sf, x: number) => Math.tanh(untagNumber(x))),
// Additional utility
deg: new LuaBuiltinFunction((_sf, x: number) =>
untagNumber(x) * 180 / Math.PI
),
rad: new LuaBuiltinFunction((_sf, x: number) =>
untagNumber(x) * Math.PI / 180
),
ult: new LuaBuiltinFunction((_sf, m: number, n: number) => {
return (untagNumber(m) >>> 0) < (untagNumber(n) >>> 0);
}),
// Keep the cosineSimilarity utility function
cosineSimilarity: new LuaBuiltinFunction(
(sf, vecA: LuaTable | number[], vecB: LuaTable | number[]) => {
if (vecA instanceof LuaTable) {
vecA = vecA.toJSArray();
}
if (vecB instanceof LuaTable) {
vecB = vecB.toJSArray();
}
if (vecA.length !== vecB.length) {
throw new LuaRuntimeError("Vectors must be of the same length", sf);
}
let dotProduct = 0;
let normA = 0;
let normB = 0;
for (let i = 0; i < vecA.length; i++) {
dotProduct += vecA[i] * vecB[i];
normA += vecA[i] ** 2;
normB += vecB[i] ** 2;
}
return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
},
),
});