isaacscript-common
Version:
Helper functions and features for IsaacScript mods.
274 lines (240 loc) • 8.78 kB
text/typescript
import type { DoorSlotFlag } from "isaac-typescript-definitions";
import { DoorSlotFlagZero, RoomShape } from "isaac-typescript-definitions";
import type { JSONEntity, JSONRoom } from "../interfaces/JSONRoomsFile";
import { sumArray } from "./array";
import { doorSlotToDoorSlotFlag, getRoomShapeDoorSlot } from "./doors";
import { isEnumValue } from "./enums";
import { addFlag } from "./flag";
import { log } from "./log";
import { getRandomFloat } from "./random";
import { parseIntSafe } from "./types";
import { assertDefined } from "./utils";
/** This represents either a `JSONRoom` or a `JSONEntity`. */
interface JSONObject {
$: { weight: string | undefined };
}
/**
* Helper function to calculate what the resulting `BitFlags<DoorSlotFlag>` value would be for a
* given JSON room.
*
* (A JSON room is an XML file converted to JSON so that it can be directly imported into your mod.)
*/
export function getJSONRoomDoorSlotFlags(
jsonRoom: JSONRoom,
): BitFlags<DoorSlotFlag> {
const roomShapeString = jsonRoom.$.shape;
const roomShape = parseIntSafe(roomShapeString);
assertDefined(
roomShape,
`Failed to parse the "shape" field of a JSON room: ${roomShapeString}`,
);
if (!isEnumValue(roomShape, RoomShape)) {
error(
`Failed to parse the "shape" field of a JSON room since it was an invalid number: ${roomShape}`,
);
}
let doorSlotFlags = DoorSlotFlagZero;
for (const door of jsonRoom.door) {
const existsString = door.$.exists;
if (existsString !== "True" && existsString !== "False") {
error(
`Failed to parse the "exists" field of a JSON room door: ${existsString}`,
);
}
if (existsString === "False") {
continue;
}
const xString = door.$.x;
const x = parseIntSafe(xString);
assertDefined(
x,
`Failed to parse the "x" field of a JSON room door: ${xString}`,
);
const yString = door.$.y;
const y = parseIntSafe(yString);
assertDefined(
y,
`Failed to parse the "y" field of a JSON room door: ${yString}`,
);
const doorSlot = getRoomShapeDoorSlot(roomShape, x, y);
assertDefined(
doorSlot,
`Failed to retrieve the door slot for a JSON room door at coordinates: [${x}, ${y}]`,
);
const doorSlotFlag = doorSlotToDoorSlotFlag(doorSlot);
doorSlotFlags = addFlag(doorSlotFlags, doorSlotFlag);
}
return doorSlotFlags;
}
/**
* Helper function to find a specific room from an array of JSON rooms.
*
* (A JSON room is an XML file converted to JSON so that it can be directly imported into your mod.)
*
* @param jsonRooms The array of rooms to search through.
* @param variant The room variant to select. (The room variant can be thought of as the ID of the
* room.)
*/
export function getJSONRoomOfVariant(
jsonRooms: readonly JSONRoom[],
variant: int,
): JSONRoom | undefined {
const jsonRoomsOfVariant = jsonRooms.filter((jsonRoom) => {
const roomVariantString = jsonRoom.$.variant;
const roomVariant = parseIntSafe(roomVariantString);
if (roomVariant === undefined) {
error(
`Failed to convert a JSON room variant to an integer: ${roomVariantString}`,
);
}
return roomVariant === variant;
});
// The room variant acts as an ID for the room. We assume that there should only be a single room
// per variant.
if (jsonRoomsOfVariant.length === 0) {
return undefined;
}
if (jsonRoomsOfVariant.length === 1) {
return jsonRoomsOfVariant[0];
}
error(
`Found ${jsonRoomsOfVariant.length} JSON rooms with a variant of ${variant}, when there should only be 1.`,
);
}
/**
* Helper function to find all of the JSON rooms that match the sub-type provided.
*
* (A JSON room is an XML file converted to JSON so that it can be directly imported into your mod.)
*
* @param jsonRooms The array of rooms to search through.
* @param subType The sub-type to match.
*/
export function getJSONRoomsOfSubType(
jsonRooms: readonly JSONRoom[],
subType: int,
): readonly JSONRoom[] {
return jsonRooms.filter((jsonRoom) => {
const roomSubTypeString = jsonRoom.$.subtype;
const roomSubType = parseIntSafe(roomSubTypeString);
if (roomSubType === undefined) {
error(
`Failed to convert a JSON room sub-type to an integer: ${roomSubTypeString}`,
);
}
return roomSubType === subType;
});
}
/**
* Helper function to get a random JSON entity from an array of JSON entities.
*
* (A JSON entity is an entity inside of a JSON room. A JSON room is an XML file converted to JSON
* so that it can be directly imported into your mod.)
*
* Note that this function does not simply choose a random element in the provided array; it will
* properly account for each room weight using the algorithm from:
* https://stackoverflow.com/questions/1761626/weighted-random-numbers
*
* If you want an unseeded entity, you must explicitly pass `undefined` to the `seedOrRNG`
* parameter.
*
* @param jsonEntities The array of entities to randomly choose between.
* @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.
* @param verbose Optional. If specified, will write entries to the "log.txt" file that describe
* what the function is doing. Default is false.
*/
export function getRandomJSONEntity(
jsonEntities: readonly JSONEntity[],
seedOrRNG: Seed | RNG | undefined,
verbose = false,
): JSONEntity {
const totalWeight = getTotalWeightOfJSONObject(jsonEntities);
if (verbose) {
log(`Total weight of the JSON entities provided: ${totalWeight}`);
}
const chosenWeight = getRandomFloat(0, totalWeight, seedOrRNG);
if (verbose) {
log(`Randomly chose weight for JSON entity: ${chosenWeight}`);
}
const randomJSONEntity = getJSONObjectWithChosenWeight(
jsonEntities,
chosenWeight,
);
assertDefined(
randomJSONEntity,
`Failed to get a JSON entity with chosen weight: ${chosenWeight}`,
);
return randomJSONEntity;
}
/**
* Helper function to get a random JSON room from an array of JSON rooms.
*
* (A JSON room is an XML file converted to JSON so that it can be directly imported into your mod.)
*
* Note that this function does not simply choose a random element in the provided array; it will
* properly account for each room weight using the algorithm from:
* https://stackoverflow.com/questions/1761626/weighted-random-numbers
*
* If you want an unseeded room, you must explicitly pass `undefined` to the `seedOrRNG` parameter.
*
* @param jsonRooms The array of rooms to randomly choose between.
* @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.
* @param verbose Optional. If specified, will write entries to the "log.txt" file that describe
* what the function is doing. Default is false.
*/
export function getRandomJSONRoom(
jsonRooms: readonly JSONRoom[],
seedOrRNG: Seed | RNG | undefined,
verbose = false,
): JSONRoom {
const totalWeight = getTotalWeightOfJSONObject(jsonRooms);
if (verbose) {
log(`Total weight of the JSON rooms provided: ${totalWeight}`);
}
const chosenWeight = getRandomFloat(0, totalWeight, seedOrRNG);
if (verbose) {
log(`Randomly chose weight for JSON room: ${chosenWeight}`);
}
const randomJSONRoom = getJSONObjectWithChosenWeight(jsonRooms, chosenWeight);
assertDefined(
randomJSONRoom,
`Failed to get a JSON room with chosen weight: ${chosenWeight}`,
);
return randomJSONRoom;
}
function getTotalWeightOfJSONObject(
jsonOjectArray: readonly JSONObject[],
): float {
const weights = jsonOjectArray.map((jsonObject) => {
const weightString = jsonObject.$.weight;
const weight = tonumber(weightString);
assertDefined(
weight,
`Failed to parse the weight of a JSON object: ${weightString}.`,
);
return weight;
});
return sumArray(weights);
}
function getJSONObjectWithChosenWeight<T extends JSONObject>(
jsonOjectArray: readonly T[],
chosenWeight: float,
): T | undefined {
let weightAccumulator = 0;
for (const jsonObject of jsonOjectArray) {
const weightString = jsonObject.$.weight;
const weight = tonumber(weightString);
assertDefined(
weight,
`Failed to parse the weight of a JSON object: ${weightString}`,
);
weightAccumulator += weight;
if (weightAccumulator >= chosenWeight) {
return jsonObject;
}
}
return undefined;
}