personae
Version:
This tool is used to generate a person either NPC or other Edit
549 lines (485 loc) • 14.5 kB
text/typescript
import Genetica, { IGeneticaOpts } from "genetica";
import {
AgeGroups,
ExpandedAlignments,
expandedAlignmentsMatrix,
standardArray,
Genders,
ShortAbilityTypes,
ILinkBackground,
ILinkKlass,
ILinkRace,
IPerson,
PersonTypes,
ILinkCulture,
IRace,
ExpandedAlignmentsY,
ExpandedAlignmentsX,
IExpandedAlignmentMatrixDetail,
IKlass,
ICulture,
IBackground,
AbilityMethods,
RacialAbilityIncreaseTypes,
IIdeals,
IDNA,
roll,
} from "opendnd-core";
import defaults, { IPersonaeDefaults } from './defaults';
import * as uuidv1 from "uuid/v1";
import Renderer from "./renderer";
import Saver from "./saver";
import "./extensions";
// this is the main class for generating a person
const randomWeighted = require("random-weighted");
const Nomina = require("nomina");
const path = require("path");
const rootDir = path.join(__dirname, "..");
const pinfo = require(path.join(rootDir, "package.json"));
export interface IPersonaeOpts {
defaults?: IPersonaeDefaults;
type?: PersonTypes;
race?: ILinkRace;
klass?: ILinkKlass;
culture?: ILinkCulture;
background?: ILinkBackground;
alignment?: ExpandedAlignments;
gender?: Genders;
name?: string;
age?: number;
ageGroup?: AgeGroups;
DNA?: IDNA;
seed?: any;
}
class Personae {
public defaults: IPersonaeDefaults;
public opts: IPersonaeOpts;
public race: IRace;
public klass: IKlass;
public background: IBackground;
public culture: ICulture;
public alignmentX: ExpandedAlignmentsX;
public alignmentY: ExpandedAlignmentsY;
// init
constructor(opts: IPersonaeOpts = {}) {
this.opts = opts;
this.defaults = opts.defaults || defaults;
}
// validate the options
public validateOpts(opts: IPersonaeOpts = {}) {
// generate default type
if (opts.type === undefined) { opts.type = PersonTypes.Playable; }
// 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 klass
if (opts.klass === undefined) opts.klass = Object.values(this.defaults.klasses).sample();
this.klass = this.defaults.klassesDict[opts.klass.uuid];
// generate random background
if (opts.background === undefined) opts.background = Object.values(this.defaults.backgrounds).sample();
this.background = this.defaults.backgroundsDict[opts.background.uuid];
// generate random culture
if (opts.culture === undefined) opts.culture = Object.values(this.defaults.cultures).sample();
this.culture = this.defaults.culturesDict[opts.culture.uuid];
// generate random alignment
if (opts.alignment === undefined) opts.alignment = Object.values(ExpandedAlignments).sample();
// generate random gender
if (opts.gender === undefined) opts.gender = Object.values(Genders).sample();
// generate random name
if (opts.name === undefined) opts.name = new Nomina().generate(); // TODO: replace w/ properties and update nomina
if (this.race === undefined) {
console.log(opts.race);
}
// generate age and ageGroup
if ((opts.age === undefined) && (opts.ageGroup === undefined)) {
opts.ageGroup = Personae.generateAgeGroup(this.race);
opts.age = Personae.generateAge(this.race, opts.ageGroup);
// generate ageGroup from age
} else if ((opts.age !== undefined) && (opts.ageGroup === undefined)) {
opts.ageGroup = Personae.generateAgeGroup(this.race);
// generate age from ageGroup
} else if ((opts.age === undefined) && (opts.ageGroup !== undefined)) {
opts.age = Personae.generateAge(this.race, opts.ageGroup);
}
this.opts = opts;
return opts;
}
// load a file and return person
public static load(filepath) {
return Saver.load(filepath);
}
// save a person
public static save(filepath, person) {
return Saver.save(filepath, person);
}
// output
public static output(person, type = "sh") {
const mdTypes = ["md", "markdown"];
// markdown
if (mdTypes.includes(type)) { return Renderer.toMarkdown(person); }
// default to console
return Renderer.toConsole(person);
}
// generate age
public static generateAge(race: IRace, ageGroup = AgeGroups.Child) {
const { ageRanges } = race;
const group = ageRanges[ageGroup];
const { min, dice } = group;
return min + roll(dice);
}
// get ageGroup from age
public static getAgeGroup(race: IRace, age: number = 1) {
const { ageRanges } = race;
const { child, young, middle, old } = ageRanges;
// check conditionals
if (age <= old.max) {
return AgeGroups.Old;
} else if (age <= middle.max) {
return AgeGroups.Middle;
} else if (age <= young.max) {
return AgeGroups.Young;
} else if (age <= child.max) {
return AgeGroups.Child;
} else {
throw new Error('There was an error with the age group maximums!');
}
}
// generate ageGroup
public static generateAgeGroup(race:IRace) {
const { ageRanges } = race;
const ageWeights = [
ageRanges.child.weight,
ageRanges.young.weight,
ageRanges.middle.weight,
ageRanges.old.weight,
];
return Object.values(AgeGroups)[randomWeighted(ageWeights)];
}
// reset opts
public resetOpts() {
this.opts = {};
this.race = undefined;
this.klass = undefined;
this.background = undefined;
this.culture = undefined;
this.alignmentX = undefined;
this.alignmentY = undefined;
}
// generate personality traits
public generatePersonalityTraits(personalityTraits = []) {
const personalityTraitA = personalityTraits.sample();
const pesronalityTraitB = personalityTraits.sample();;
// if it's the same then try again
if (personalityTraitA === pesronalityTraitB) { return this.generatePersonalityTraits(personalityTraits); }
return [personalityTraitA, pesronalityTraitB];
}
// generate ideal
public generateIdeal(alignment:ExpandedAlignments, ideals:IIdeals) {
const alignmentDetail:IExpandedAlignmentMatrixDetail = expandedAlignmentsMatrix[alignment];
this.alignmentX = alignmentDetail.x;
this.alignmentY = alignmentDetail.y;
// generate a sample set to choose from at random
let sampleSet = ideals.any.concat(ideals[this.alignmentX]).concat(ideals[this.alignmentY]);
if (sampleSet.length < 2) {
sampleSet = this.defaults.ideals.any.concat(this.defaults.ideals[this.alignmentX])
.concat(this.defaults.ideals[this.alignmentY]);
}
// TODO: clean this up, happening due to impure/social, etc coming back as undeined
const finalSet = [];
sampleSet.forEach((el) => {
if (el === undefined) return;
finalSet.push(el);
});
return finalSet.sample();
}
// calculate modifier
public calculateMod(score = 0) {
return Math.floor((score - 10) / 2);
}
// generate abilities
public generateAbilities(method:string = AbilityMethods.StandardArray) {
const { abilitiyIncreases } = this.race;
// setup the ability object
const abilities = {
STR: 10,
DEX: 10,
CON: 10,
INT: 10,
WIS: 10,
CHA: 10,
};
// TODO: implement other methods of generating abilities
if (method === AbilityMethods.StandardArray) {
const available = Object.assign([], Object.keys(ShortAbilityTypes));
// iterate through each score in the standard array
standardArray.forEach((score) => {
const ability = available.sample();
abilities[ability] = score;
available.splice(available.indexOf(ability), 1);
});
} else {
throw new Error('Method not implemented for ability generation!');
}
// compute the racial ability increase
abilitiyIncreases.forEach((rule) => {
const { ability, amount } = rule;
// increase all
if (ability === RacialAbilityIncreaseTypes.All) {
Object.keys(abilities).forEach((key) => {
abilities[key] += amount;
})
// increase the ability of your choice
} else if (ability === RacialAbilityIncreaseTypes.Choice) {
abilities[Object.keys(abilities).sample()] += amount;
// increase the specific ability
} else {
abilities[ShortAbilityTypes[ability]] += amount;
}
});
// TODO: compute ability increases from other sources as well
return abilities;
}
// generate a child
public generateChild(opts: any = {}, motherPerson: any = {}, fatherPerson: any = {}) {
if (motherPerson.DNA.gender !== Genders.Female) { throw new Error("Mother is not female!"); }
if (fatherPerson.DNA.gender !== Genders.Male) { throw new Error("Father is not male!"); }
this.validateOpts(Object.assign(this.opts, opts));
const { culture } = motherPerson;
const { race } = motherPerson.DNA;
const { gender } = this.opts;
// generate DNA from mother and father
const genetica = new Genetica({
race,
gender,
});
const DNA = genetica.generateChild({}, motherPerson.DNA, fatherPerson.DNA);
const child = this.generate({
culture,
race,
gender,
DNA,
});
return child;
}
// generate parents
public generateParents(person) {
const { DNA, type, culture } = person;
const { race } = DNA;
const raceLink:ILinkRace = {
uuid: race.uuid,
name: race.name,
};
const geneticaOpts:IGeneticaOpts = {
race: raceLink,
};
const genetica = new Genetica(geneticaOpts);
const parentsDNA = genetica.generateParents(DNA);
const mother = this.generate({
culture,
type,
race,
gender: Genders.Female,
DNA: parentsDNA.motherDNA,
});
const father = this.generate({
culture,
type,
race,
gender: Genders.Male,
DNA: parentsDNA.fatherDNA,
});
return {
mother,
father,
};
}
// generate a person
public generate(opts = {}): IPerson {
const uuid = uuidv1();
const genOpts = this.validateOpts(Object.assign(this.opts, opts));
const { version } = pinfo;
const { type, alignment, name, gender, age, ageGroup } = genOpts;
const { race, background, culture, klass } = this;
// generate person details
const abilities = this.generateAbilities();
const specialty = background.specialties.sample();
const personalityTraits = this.generatePersonalityTraits(background.personalityTraits);
const ideal = this.generateIdeal(alignment, background.ideals);
const bond = background.bonds.sample();
const flaw = background.flaws.sample();
const mannerism = this.defaults.mannerisms.sample();
const talent = this.defaults.talents.sample();
const trait = this.defaults.traits.sample();
const characteristic = this.defaults.characteristics.sample();
// generate DNA
const geneticaOpts:IGeneticaOpts = {
race: genOpts.race,
gender,
};
const genetica = new Genetica(geneticaOpts);
let DNA = this.opts.DNA || genetica.generate();
if (this.opts.seed) { DNA = genetica.generate(Object.assign(geneticaOpts, this.opts.seed)); } // set DNA to the seed if we have it
// after generating a person reset DNA
this.resetOpts();
return {
version,
uuid,
culture,
name,
age,
ageGroup,
abilities,
type,
alignment,
klass,
background,
specialty,
personalityTraits,
ideal,
bond,
flaw,
mannerism,
talent,
trait,
characteristic,
DNA,
// TODO: add new fields
abstract: false,
level: 0,
XP: 0,
playerName: "",
power: 0,
honor: 0,
piety: 0,
reputation: 0,
treasury: {
cp: 0,
sp: 0,
ep: 0,
gp: 0,
pp: 0,
},
cost: 0,
proficiencies: {
skills: [],
languages: [],
armors: [],
weapons: [],
transportation: [],
tools: [],
bonus: 2,
},
initiative: 0,
speed: 0,
AC: 0,
hitDice: [],
maxHP: 0,
tempHP: 0,
HP: 0,
conditions: [],
exhaustion: 0,
resistance: null,
vulnerability: null,
spellcasting: {
ability: null,
saveDC: 0,
attackModifier: 0,
spells: [],
},
faith: null,
mother: null,
father: null,
siblings: [],
spouse: null,
children: [],
family: null,
liege: null,
allies: [],
enemies: [],
factions: {
memberOf: [],
allies: [],
enemies: [],
},
birth: {
domain: null,
date: null,
rank: 0,
},
death: {
domain: null,
date: null,
},
features: [],
actions: [],
items: [],
magicItems: [],
weight: 0,
capacity: 0,
equipment: {
head: null,
leftBrow: null,
leftEye: null,
leftEar: null,
rightBrow: null,
rightEye: null,
rightEar: null,
eyes: null,
nose: null,
mouth: null,
chin: null,
neck: null,
leftShoulder: null,
leftBreast: null,
leftArm: null,
leftWrist: null,
leftHand: null,
leftFingers: null,
leftGrip: null,
rightShoulder: null,
rightBreast: null,
rightArm: null,
rightWrist: null,
rightHand: null,
rightFingers: null,
rightGrip: null,
torso: null,
back: null,
abdomen: null,
waist: null,
groin: null,
rear: null,
leftThigh: null,
leftLeg: null,
leftKnee: null,
leftShin: null,
leftAnkle: null,
leftFoot: null,
leftToes: null,
rightThigh: null,
rightLeg: null,
rightKnee: null,
rightShin: null,
rightAnkle: null,
rightFoot: null,
rightToes: null,
mount: null,
},
chattel: [],
domains: [],
buildings: [],
titles: [],
familiars: [],
vehicles: [],
knowledge: [],
backstory: "",
campaigns: [],
activeCampaign: null,
quests: [],
stories: [],
dialogs: [],
currentDialog: 0,
};
}
}
export default Personae;