isaacscript-common
Version:
Helper functions and features for IsaacScript mods.
194 lines (170 loc) • 6.18 kB
text/typescript
import type {
CopyableIsaacAPIClassType,
Direction,
} from "isaac-typescript-definitions";
import { SerializationBrand } from "../enums/private/SerializationBrand";
import { angleToDirection } from "./direction";
import { isIsaacAPIClassOfType, isaacAPIClassEquals } from "./isaacAPIClass";
import { getRandomFloat } from "./random";
import { isRNG, newRNG } from "./rng";
import {
copyUserdataValuesToTable,
getNumbersFromTable,
tableHasKeys,
} from "./table";
import { isTable } from "./types";
import { assertDefined } from "./utils";
export type SerializedVector = LuaMap<string, unknown> & {
readonly __serializedVectorBrand: symbol;
readonly __kind: CopyableIsaacAPIClassType.VECTOR;
};
const OBJECT_NAME = "Vector";
const KEYS = ["X", "Y"] as const;
/** Helper function to copy a `Vector` Isaac API class. */
export function copyVector(vector: Vector): Vector {
if (!isVector(vector)) {
error(
`Failed to copy a ${OBJECT_NAME} object since the provided object was not a userdata ${OBJECT_NAME} class.`,
);
}
return Vector(vector.X, vector.Y);
}
/**
* Helper function to convert a `SerializedVector` object to a normal `RNG` object. (This is used by
* the save data manager when reading data from the "save#.dat" file.)
*/
export function deserializeVector(vector: SerializedVector): Vector {
if (!isTable(vector)) {
error(
`Failed to deserialize a ${OBJECT_NAME} object since the provided object was not a Lua table.`,
);
}
const [x, y] = getNumbersFromTable(vector, OBJECT_NAME, ...KEYS);
assertDefined(
x,
`Failed to deserialize a ${OBJECT_NAME} object since the provided object did not have a value for: X`,
);
assertDefined(
y,
`Failed to deserialize a ${OBJECT_NAME} object since the provided object did not have a value for: Y`,
);
return Vector(x, y);
}
/**
* Helper function to measure a vector to see if it has a non-zero length using a threshold to
* ignore extremely small values.
*
* Use this function instead of explicitly checking if the length is 0 because vectors in the game
* are unlikely to ever be exactly set to 0. Instead, they will always have some miniscule length.
*
* @param vector The vector to measure.
* @param threshold Optional. The threshold from 0 to consider to be a non-zero vector. Default is
* 0.01.
*/
export function doesVectorHaveLength(
vector: Vector,
threshold = 0.01,
): boolean {
return vector.Length() >= threshold;
}
/**
* Given an array of vectors, this helper function returns the closest one to a provided reference
* vector.
*
* @param referenceVector The vector to compare against.
* @param vectors The array of vectors to look through.
*/
export function getClosestVectorTo(
referenceVector: Vector,
vectors: readonly Vector[],
): Vector | undefined {
let closestVector: Vector | undefined;
let closestDistance = Number.POSITIVE_INFINITY;
for (const vector of vectors) {
const distance = referenceVector.Distance(vector);
if (distance < closestDistance) {
closestVector = vector;
closestDistance = distance;
}
}
return closestVector;
}
/**
* Helper function to get a random vector between (-1, -1) and (1, 1).
*
* To get random vectors with a bigger length, multiply this with a number.
*
* Use this over the `RandomVector` function when you need the vector to be seeded.
*
* If you want to generate an unseeded vector, you must explicitly pass `undefined` to the
* `seedOrRNG` parameter.
*
* @param seedOrRNG The `Seed` or `RNG` object to use. If an `RNG` object is provided, the
* `RNG.Next` method will be called. If `undefined` is provided, it will default to
* a random seed.
*/
export function getRandomVector(
seedOrRNG: Seed | RNG | undefined,
): Readonly<Vector> {
const rng = isRNG(seedOrRNG) ? seedOrRNG : newRNG(seedOrRNG);
const x = getRandomFloat(-1, 1, rng);
const y = getRandomFloat(-1, 1, rng);
return Vector(x, y);
}
/**
* Used to determine is the given table is a serialized `Vector` object created by the `deepCopy`
* function.
*/
export function isSerializedVector(
object: unknown,
): object is SerializedVector {
if (!isTable(object)) {
return false;
}
return tableHasKeys(object, ...KEYS) && object.has(SerializationBrand.VECTOR);
}
/** Helper function to check if something is an instantiated `Vector` object. */
export function isVector(object: unknown): object is Vector {
return isIsaacAPIClassOfType(object, OBJECT_NAME);
}
/**
* Helper function to convert a `Vector` object to a `SerializedVector` object. (This is used by the
* save data manager when writing data from the "save#.dat" file.)
*/
export function serializeVector(vector: Vector): SerializedVector {
if (!isVector(vector)) {
error(
`Failed to serialize a ${OBJECT_NAME} object since the provided object was not a userdata ${OBJECT_NAME} class.`,
);
}
const vectorTable = new LuaMap<string, unknown>();
copyUserdataValuesToTable(vector, KEYS, vectorTable);
vectorTable.set(SerializationBrand.VECTOR, "");
return vectorTable as SerializedVector;
}
/**
* Helper function to compare two vectors for equality.
*
* This function is useful because vectors are not directly comparable. In other words, `Vector(1.2)
* === Vector(1.2)` will be equal to false.
*/
export function vectorEquals(vector1: Vector, vector2: Vector): boolean {
return isaacAPIClassEquals(vector1, vector2, KEYS);
}
/** Helper function for finding out which way a vector is pointing. */
export function vectorToDirection(vector: Vector): Direction {
const angleDegrees = vector.GetAngleDegrees();
return angleToDirection(angleDegrees);
}
/**
* Helper function to convert a vector to a string.
*
* @param vector The vector to convert.
* @param round Optional. If true, will round the vector values to the nearest integer. Default is
* false.
*/
export function vectorToString(vector: Vector, round = false): string {
const x = round ? Math.round(vector.X) : vector.X;
const y = round ? Math.round(vector.Y) : vector.Y;
return `(${x}, ${y})`;
}