UNPKG

@opendnd/genetica

Version:

This is a tool for using a simplified genetics system to generate inheritable traits for DnD characters.

568 lines (472 loc) 16.7 kB
import { Categories, DiceAndSex, DNA, Genders, ILinkRace, IRace, roll, } from "@opendnd/core"; import * as path from "path"; import * as uuidv1 from "uuid/v1"; import defaults, { IGeneticaDefaults } from "./defaults"; import Saver from "./saver"; import SaverSeed from "./saver-seed"; import "./extensions"; const Roll = require("roll"); // tslint:disable-line const rootDir = path.join(__dirname, ".."); const pinfo = require(path.join(rootDir, "package.json")); // tslint:disable-line // opts for genetica export interface IGeneticaOpts { defaults?: IGeneticaDefaults; race?: ILinkRace; gender?: Genders; mutation?: string; } // this is the main class for generating genetics class Genetica { // load a file and return DNA public static load(filepath) { return Saver.load(filepath); } // load a seed file public static loadSeed(filepath) { return SaverSeed.load(filepath); } public defaults: IGeneticaDefaults; public opts: IGeneticaOpts; public race: IRace; public gender: Genders; // init constructor(opts: IGeneticaOpts = {}) { this.opts = opts; this.defaults = opts.defaults || defaults; } // validate the options public validateOpts(opts: IGeneticaOpts = {}) { // generate random race if (opts.race === undefined) { opts.race = Object.values(this.defaults.races).sample(); } this.race = this.defaults.racesDict[opts.race.uuid]; // generate random gender if (opts.gender === undefined) { opts.gender = Object.values(Genders).sample(); } this.gender = opts.gender; // check chromosomes Object.keys(opts).forEach((opt) => { if (opt.includes("chromosome-")) { if (!opts[opt].includes("=")) { throw new Error("Malformatted chromosome input!"); } // get parts const parts = opts[opt].split("="); let partA = parts[0]; let partB = parts[1]; // get the template const template = this.race; const c = opt.replace("chromosome-", ""); // assign dice value unless it's the sex chromosome // otherwise set the x and y dice let dice; let diceValue; let Xdice; let XdiceValue; let Ydice; let YdiceValue; if (c !== DiceAndSex.Sex) { dice = template.chromosomes[c]; diceValue = parseInt(dice.replace("d", ""), 10); } else { Xdice = template.sex.x; XdiceValue = parseInt(Xdice.replace("d", ""), 10); Ydice = template.sex.y; YdiceValue = parseInt(Ydice.replace("d", ""), 10); } // check sex chromosomes if (opt === "chromosome-sex") { if (opts.gender === Genders.Female) { if (!partA.includes("X")) { throw new Error(`Malformatted female sex chromosome input "${partA}"!`); } if (!partB.includes("X")) { throw new Error(`Malformatted female sex chromosome input "${partB}"!`); } partA = parseInt(partA.replace("X", ""), 10); partB = parseInt(partB.replace("X", ""), 10); if (partA > XdiceValue) { throw new Error(`Sex chromosome value "${partA}" higher than possible dice roll of "${XdiceValue}"!`); } if (partB > XdiceValue) { throw new Error(`Sex chromosome value "${partB}" higher than possible dice roll of "${XdiceValue}"!`); } } if (opts.gender === Genders.Male) { if (!partA.includes("X")) { throw new Error(`Malformatted male sex chromosome input "${partA}"!`); } if (!partB.includes("Y")) { throw new Error(`Malformatted male sex chromosome input "${partB}"!`); } partA = parseInt(partA.replace("X", ""), 10); partB = parseInt(partB.replace("Y", ""), 10); if (partA > XdiceValue) { throw new Error(`Sex chromosome value "${partA}" higher than possible dice roll of "${XdiceValue}"!`); } if (partB > YdiceValue) { throw new Error(`Sex chromosome value "${partB}" higher than possible dice roll of "${YdiceValue}"!`); } } } else { if (partA > diceValue) { throw new Error(`Chromosome value "${partA}" higher than possible dice roll of "${diceValue}"!`); } if (partB > diceValue) { throw new Error(`Chromosome value "${partB}" higher than possible dice roll of "${diceValue}"!`); } } } }); this.opts = opts; return opts; } // reset opts public resetOpts() { this.opts = {}; this.gender = undefined; this.race = undefined; } // map chromosomes to opts public mapChromosomesToOpts(chromosomes = {}) { const template = this.race; const chromosomeOpts = {}; Object.keys(chromosomes).forEach((c) => { const dice = template.chromosomes[c]; let chromosomeName = `chromosome-${c}`; if (dice === DiceAndSex.Sex) { chromosomeName = "chromosome-sex"; } chromosomeOpts[chromosomeName] = chromosomes[c]; }); return chromosomeOpts; } // generate child chromosomes public generateChildChromosomes(motherChromosomes = {}, fatherChromosomes = {}) { const template = this.race; const chromosomes = Object.assign({}, template.chromosomes); // assign chromosome from the mother and the father Object.keys(template.chromosomes).forEach((c) => { const dice = template.chromosomes[c]; // get mother chromosome parts const motherParts = motherChromosomes[c].split("="); const fatherParts = fatherChromosomes[c].split("="); // assign the X from the father and a random X from the mother if (dice === DiceAndSex.Sex) { if (this.gender === Genders.Female) { chromosomes[c] = `${motherParts.sample()}=${fatherParts[0]}`; return; } // always assign the way from the father chromosomes[c] = `${motherParts.sample()}=${fatherParts[1]}`; return; } // return the random chromosome parts chromosomes[c] = `${motherParts.sample()}=${fatherParts.sample()}`; }); return chromosomes; } // generate parent chromosomes public generateParentChromosomes(childChromosomes = {}) { const template = this.race; const Xdice = template.sex.x; const Ydice = template.sex.y; const motherChromosomes = {}; const fatherChromosomes = {}; // generate the other half of chromosomes for the mother and father Object.keys(template.chromosomes).forEach((c) => { const dice = template.chromosomes[c]; const parts = childChromosomes[c].split("="); const partA = parts[0]; const partB = parts[1]; let chromosomeName; // handle the sex chromosome differently if (dice === DiceAndSex.Sex) { chromosomeName = "chromosome-sex"; if (this.gender === Genders.Male) { const XRes = roll(`1${Xdice}`); fatherChromosomes[chromosomeName] = `X${XRes}=${partB}`; // flip a coins on where to assign the chromosome if (roll("1d2") === 1) { motherChromosomes[chromosomeName] = `${partA}=X${XRes}`; } else { motherChromosomes[chromosomeName] = `X${XRes}=${partA}`; } } else if (this.gender === Genders.Female) { const XRes = roll(`1${Xdice}`); const YRes = roll(`1${Ydice}`); // give one X to the father and the other to the mother let fatherX; let motherX; if (roll("1d2") === 1) { fatherX = partA; motherX = partB; } else { fatherX = partB; motherX = partA; } // set the father X fatherChromosomes[chromosomeName] = `${fatherX}=Y${YRes}`; // generate chromosomes as normal for the mother if (roll("1d2") === 1) { motherChromosomes[chromosomeName] = `X${XRes}=${motherX}`; } else { motherChromosomes[chromosomeName] = `${motherX}=X${XRes}`; } } return; } // regular chromosomes chromosomeName = `chromosome-${c}`; // get a regular roll for the father const fatherA = roll(`1${dice}`); let fatherB; const motherA = roll(`1${dice}`); let motherB; // give one to the father and one to the mother if (roll("1d2") === 1) { fatherB = partA; motherB = partB; } else { fatherB = partB; motherB = partA; } // generate chromosomes as normal for the father if (roll("1d2") === 1) { fatherChromosomes[chromosomeName] = `${fatherA}=${fatherB}`; } else { fatherChromosomes[chromosomeName] = `${fatherB}=${fatherA}`; } // generate chromosomes as normal for the mother if (roll("1d2") === 1) { motherChromosomes[chromosomeName] = `${motherA}=${motherB}`; } else { motherChromosomes[chromosomeName] = `${motherB}=${motherA}`; } }); return { fatherChromosomes, motherChromosomes, }; } // generate chromosomes public generateChromosomes() { const template = this.race; const Xdice = template.sex.x; const Ydice = template.sex.y; const chromosomes = Object.assign({}, template.chromosomes); // assign the roles to each chromosome Object.keys(template.chromosomes).forEach((c) => { const dice = template.chromosomes[c]; let mutation = false; // check for mutation to bypass passed chromosome if (this.opts.mutation) { const mutationRoll = roll(`1${this.opts.mutation}`); if (mutationRoll === 1) { mutation = true; } } // check passed opts for sex chromosome if ((dice === DiceAndSex.Sex) && (this.opts["chromosome-sex"]) && !mutation) { chromosomes[c] = this.opts["chromosome-sex"]; return; } // check for other passed chromosomes if ((this.opts[`chromosome-${c}`]) && !mutation) { chromosomes[c] = this.opts[`chromosome-${c}`]; return; } // do special roll for sex chromosomes if (dice === DiceAndSex.Sex) { if (this.gender === Genders.Female) { const res = new Roll().roll(`2${Xdice}`).rolled; chromosomes[c] = `X${res[0]}=X${res[1]}`; return; } const XRes = roll(`1${Xdice}`); const YRes = roll(`1${Ydice}`); chromosomes[c] = `X${XRes}=Y${YRes}`; return; } // standard do here chromosomes[c] = new Roll().roll(`2${dice}`).rolled.join("="); }); return chromosomes; } // generate height and weight public generateHeightAndWeight(race: IRace) { const { height: tplHeight, weight: tplWeight } = this.race; /** * The roll given in the Height Modifier column determines * the character's extra height (in inches) beyond the base height. * * That same number multiplied by the dice roll or quantity given in * the Weight Modifier column determines the character's extra weight * (in pounds) beyond the base weight. */ const height = tplHeight.base + roll(tplHeight.dice); let weight: number; if (tplWeight.multiplier) { weight = tplWeight.base + (height * tplWeight.multiplier); } else { weight = tplWeight.base + (height * roll(tplWeight.dice)); } return { height, weight, }; } // generate traits from chromosomes public generateTraits(chromosomes = {}) { const template = this.race; const traits = {}; // exit if the genes dictionary isn't defined yet if (template.dictionary === undefined) { return traits; } // 3=9 // X1=Y3 // iterate on the categories Object.keys(template.categories).forEach((categoryName) => { const chromosome = template.categories[categoryName]; const rolls = chromosomes[chromosome]; const parts = rolls.split("="); const partA = parts[0]; const partB = parts[1]; let a = parts[0]; let b = parts[1]; let dominant = partB; let commonGene = `${categoryName}:C${chromosome}:`; let rareGene = `${categoryName}:C${chromosome}:`; // don't give facial hair to females if ((this.gender === Genders.Female) && (categoryName === Categories.HairFacial)) { return; } // female if (this.gender === Genders.Female) { a = a.replace("X", ""); b = b.replace("X", ""); // male } else if (this.gender === Genders.Male) { a = a.replace("X", ""); b = b.replace("Y", ""); } // convert to int a = parseInt(a, 10); b = parseInt(b, 10); // set a to dominant if it's higher if (a > b) { dominant = partA; } // add an extra X for female dominant genes if ((this.gender === Genders.Female) && (categoryName === Categories.Sex)) { commonGene += "X"; } commonGene += dominant; rareGene += rolls; // look for a rare trait if (Object.keys(template.dictionary).indexOf(rareGene) >= 0) { traits[categoryName] = { gene: rareGene, trait: template.dictionary[rareGene], }; return; } // look for a common trait if (Object.keys(template.dictionary).indexOf(commonGene) >= 0) { traits[categoryName] = { gene: commonGene, trait: template.dictionary[commonGene], }; } }); return traits; } // generate a child public generateChild(opts: IGeneticaOpts, motherDNA: DNA, fatherDNA: DNA) { // check for proper inputs if (motherDNA.race.uuid !== fatherDNA.race.uuid) { throw new Error("Cross-breeding between races is not yet supported!"); } if (motherDNA.gender !== Genders.Female) { throw new Error("The mother is not female!"); } if (fatherDNA.gender !== Genders.Male) { throw new Error("The father is not male!"); } // get other data this.opts.race = motherDNA.race; this.validateOpts(Object.assign(this.opts, opts)); const { race, gender } = this.opts; // generate chromosomes let chromosomes = this.generateChildChromosomes(motherDNA.chromosomes, fatherDNA.chromosomes); chromosomes = this.mapChromosomesToOpts(chromosomes); const childOpts = Object.assign({ gender, race, }, chromosomes); return this.generate(childOpts); } // generate parents public generateParents(childDNA: DNA) { const { race, gender, chromosomes } = childDNA; this.opts = { gender, race, }; this.race = this.defaults.racesDict[race.uuid]; // generate mother and father chromosomes const { motherChromosomes, fatherChromosomes } = this.generateParentChromosomes(chromosomes); // generate mother const motherOpts = { gender: Genders.Female, race, }; const motherDNA = this.generate(Object.assign(motherOpts, motherChromosomes)); // generate father const fatherOpts = { gender: Genders.Male, race, }; const fatherDNA = this.generate(Object.assign(fatherOpts, fatherChromosomes)); return { fatherDNA, motherDNA, }; } // generate a person's DNA public generate(opts: IGeneticaOpts = {}): DNA { const { version } = pinfo; const genOpts: IGeneticaOpts = this.validateOpts(Object.assign(this.opts, opts)); const raceLink: ILinkRace = genOpts.race; const gender = this.gender; let raceData = this.race; // add subrace if we have it if (raceData.subraces === undefined) { raceData.subraces = []; } if (raceData.subraces.length > 0) { const subrace = raceData.subraces.sample(); // merge the subrace raceData = Object.assign(raceData, subrace); raceLink.subrace = { uuid: subrace.uuid, name: subrace.name, }; } // set the standard values const chromosomes = this.generateChromosomes(); const traits = this.generateTraits(chromosomes); const uuid = uuidv1(); const { size } = raceData; const { weight, height } = this.generateHeightAndWeight(raceData); // set the result const result: DNA = { version, uuid, name: uuid, race: raceLink, gender, chromosomes, traits, size, weight, height, abstract: false, }; // reset the opts before closing out this.resetOpts(); return result; } } export default Genetica;