convert.js

/**
 * A class for converting the relevant data in scrying workshop links and dragon profile pages into indices in the arrays of module:FRjs/data, and for converting those indices into a functioning scrying workshop link. See the `DragonTraits` class' documentation for more details. See the tutorial for usage examples.
 *
 * @module FRjs/convert
 * @tutorial 07-fr-convert
 * @requires module:FRjs/data
 */

import * as FR from "./data.js";


/** A class representing the traits of a single dragon within the context of FRjs. The class is intended to convert between indices in FRjs/data arrays, dragon profile pages, and scrying workshop links; hence, the trait data is immutable after object construction and all objects should be short-lived.
 *
 * | Tutorials | {@tutorial 07-fr-convert} |
 * |---|-| */
export class DragonTraits {
	#breed = 0;
	#eye = 0;
	#element = 0;
	#gender = 0;
	#age = 0;
	#colour = {primary: 0, secondary: 0, tertiary: 0};
	#gene = {primary: 0, secondary: 0, tertiary: 0};

	/** Returns a {@link module:FRjs/convert.DragonTraits DragonTraits} object containing all the traits defined in the given scrying workshop link.
	 * @param {string} link A scrying workshop link. NOT a link to a saved morphology; a URL for exact morphology parameters.
	 * @returns {DragonTraits} */
	static fromScrylink(link) {
		// UNUSED TRAITS:
		// gender, age, element

		const url = Object.fromEntries((new URL(link)).searchParams);
		for (const k in url) {
			url[k] = parseInt?.(url[k]);
		}

		const breed = FR.BREEDS.findIndex(x => url.breed === x.sid);

		return new DragonTraits({
			breed: breed,
			eye: FR.EYES.findIndex(x => url.eyetype === x.sid),
			element: FR.ELEMENTS.findIndex(x => url.element === x.sid),
			age: FR.AGES.findIndex(x => url.age === x.sid),
			gender: FR.GENDERS.findIndex(x => url.gender === x.sid),
			colour: {
				primary: FR.COLOURS.findIndex(x => url.body === x.sid),
				secondary: FR.COLOURS.findIndex(x => url.wings === x.sid),
				tertiary: FR.COLOURS.findIndex(x => url.tert === x.sid)
			},
			gene: {
				primary: FR.GENES.primary.findIndex(x => url.bodygene === x.sidForBreed(breed)),
				secondary: FR.GENES.secondary.findIndex(x => url.winggene === x.sidForBreed(breed)),
				tertiary: FR.GENES.tertiary.findIndex(x => url.tertgene === x.sidForBreed(breed))
			}
		});
	}

	/** Returns a {@link module:FRjs/convert.DragonTraits DragonTraits} object containing all traits defined in the contents of the given dragon profile. Note: gender is not present in text on dragon profiles, and will be the default of Male.
	 * @param {string} profile The text contents of a dragon's profile page. NOT the page HTML; what you get by selecting all text on the page in the browser window and copying it.
	 * @returns {DragonTraits} */
	static fromProfile(profile) {
		const profileRegex = /Primary Gene\n(\w+)\n(\w+)(?: \(\w+\))*\nSecondary Gene\n(\w+)\n(\w+)(?: \(\w+\))*\nTertiary Gene\n(\w+)\n(\w+).*(?:Breed\n){2}(\w+)\n(\w+)\nEye Type\n(?:Special )*Eye Type\n(\w+)\n(\w+)/s;
		const matches = profileRegex.exec(profile.replace(/\r/g, ""));
		// For some reason this age is "Dragon" in the scryshop, and "Adult" on profiles.
		matches[7] = matches[7] === "Adult" ? "Dragon" : matches[7];

		return new DragonTraits({
			breed: FR.BREEDS.findIndex(x => matches[8] === x.name),
			eye: FR.EYES.findIndex(x => matches[10] === x.name),
			element: FR.ELEMENTS.findIndex(x => matches[9] === x.name),
			age: FR.AGES.findIndex(x => matches[7] === x.name),
			colour: {
				primary: FR.COLOURS.findIndex(x => matches[1] === x.name),
				secondary: FR.COLOURS.findIndex(x => matches[3] === x.name),
				tertiary: FR.COLOURS.findIndex(x => matches[5] === x.name)
			},
			gene: {
				primary: FR.GENES.primary.findIndex(x => matches[2] === x.name),
				secondary: FR.GENES.secondary.findIndex(x => matches[4] === x.name),
				tertiary: FR.GENES.tertiary.findIndex(x => matches[6] === x.name)
			}
		});
	}

	/** Constructs a formal DragonTraits object from a generic object containing indices in FRjs/data arrays for any/all of a single dragon's traits. Calling the constructor directly is useful for converting traits into scrying workshop links, and for quickly getting the actual data objects for all traits.
	 *
	 * Any traits that are left undefined, or which are invalid, will be set to a default value; index 0 for most traits, and for genes the index of Basic.
	 * @param {{breed: number, eye: number, element: number, gender: number, age: number, colour: {primary: number, secondary: number, tertiary: number}, gene: {primary: number, secondary: number, tertiary: number}}} indices An object defining any/all of a single dragon's traits. It may contain any of the following keys, with values being indices in the appropriate array from {@link module:FRjs/data FRjs/data}:
	 * ```js
	 * {
	 * 	breed: number,
	 * 	eye: number,
	 * 	element: number,
	 * 	gender: number,
	 * 	age: number,
	 * 	colour: {
	 * 		primary: number,
	 * 		secondary: number,
	 * 		tertiary: number
	 * 	},
	 * 	gene: {
	 * 		primary: number,
	 * 		secondary: number,
	 * 		tertiary: number
	 * 	}
	 * } */
	constructor(indices) {
		if (indices.breed in FR.BREEDS) {
			this.#breed = indices.breed;
		}
		if (indices.eye in FR.EYES) {
			this.#eye = indices.eye;
		}
		if (indices.element in FR.ELEMENTS) {
			this.#element = indices.element;
		}
		if (indices.gender in FR.GENDERS) {
			this.#gender = indices.gender;
		}
		if (indices.age in FR.AGES) {
			this.#age = indices.age;
		}

		// set colours and genes, ignoring any keys in the input that we can't actually use
		for (const slot in FR.GENES) {
			if (indices?.colour?.[slot] in FR.COLOURS) {
				this.#colour[slot] = indices.colour[slot];
			}
		}
		for (const slot in FR.GENES) {
			if (FR.GENES[slot][indices?.gene?.[slot]]?.sidForBreed(this.#breed) !== undefined) {
				this.#gene[slot] = indices.gene[slot];
			}
			else { // default gene is Basic
				this.#gene[slot] = FR.GENES[slot].findIndex(x => x.sids.M === 0);
			}
		}
	}

	/** Object containing all traits as **indices** in the applicable array from {@link module:FRjs/data FRjs/data}.
	 *
	 * The structure of the object is:
	 * ```js
	 * {
	 * 	breed: number,
	 * 	eye: number,
	 * 	element: number,
	 * 	gender: number,
	 * 	age: number,
	 * 	colour: {
	 * 		primary: number,
	 * 		secondary: number,
	 * 		tertiary: number
	 * 	},
	 * 	gene: {
	 * 		primary: number,
	 * 		secondary: number,
	 * 		tertiary: number
	 * 	}
	 * }
	 * ```
	 * @type {{breed: number, eye: number, element: number, gender: number, age: number, colour: {primary: number, secondary: number, tertiary: number}, gene: {primary: number, secondary: number, tertiary: number}}} */
	get indices() {
		return {
			breed: this.#breed,
			eye: this.#eye,
			element: this.#element,
			gender: this.#gender,
			age: this.#age,
			colour: {...this.#colour},
			gene: {...this.#gene}
		};
	}

	/** An object containing all traits as **data objects** from {@link module:FRjs/data FRjs/data}.
	 *
	 * The structure of the object is:
	 * ```js
	 * {
	 * 	breed: FR.Breed,
	 * 	eye: FR.EyeType,
	 * 	element: FR.BasicTrait,
	 * 	gender: FR.BasicTrait,
	 * 	age: FR.BasicTrait,
	 * 	colour: {
	 * 		primary: FR.Colour,
	 * 		secondary: FR.Colour,
	 * 		tertiary: FR.Colour
	 * 	},
	 * 	gene: {
	 * 		primary: FR.Gene,
	 * 		secondary: FR.Gene,
	 * 		tertiary: FR.Gene
	 * 	}
	 * }
	 * ```
	 * @type {{breed: FR.Breed, eye: FR.EyeType, element: FR.BasicTrait, gender: FR.BasicTrait, age: FR.BasicTrait, colour: {primary: FR.Colour, secondary: FR.Colour, tertiary: FR.Colour}, gene: {primary: FR.Gene, secondary: FR.Gene, tertiary: FR.Gene}}} */
	get values() {
		const idxs = this.indices;
		return {
			breed: FR.BREEDS[idxs.breed],
			eye: FR.EYES[idxs.eye],
			element: FR.ELEMENTS[idxs.element],
			gender: FR.GENDERS[idxs.gender],
			age: FR.AGES[idxs.age],
			colour: {
				primary: FR.COLOURS[idxs.colour.primary],
				secondary: FR.COLOURS[idxs.colour.secondary],
				tertiary: FR.COLOURS[idxs.colour.tertiary]
			},
			gene: {
				primary: FR.GENES.primary[idxs.gene.primary],
				secondary: FR.GENES.secondary[idxs.gene.secondary],
				tertiary: FR.GENES.tertiary[idxs.gene.tertiary]
			}
		};
	}

	/** A link to the scrying workshop for a dragon with all defined traits.
	 * @type {string} */
	get scrylink() {
		const bi = this.indices.breed;
		const {breed, eye, element, gender, age, colour, gene} = this.values;
		const params = new URLSearchParams({
			breed: breed.sid,
			gender: gender.sid,
			age: age.sid,
			bodygene: gene.primary.sidForBreed(bi),
			body: colour.primary.sid,
			winggene: gene.secondary.sidForBreed(bi),
			wings: colour.secondary.sid,
			tertgene: gene.tertiary.sidForBreed(bi),
			tert: colour.tertiary.sid,
			element: element.sid,
			eyetype: eye.sid
		});
		return `https://www1.flightrising.com/scrying/predict?${params}`;
	}
}