import { createCharacter } from "./create_character.js";
import { createCharacterName } from "./create_character_name.js";
import { findRelatedProfession } from "./create_profession.js";
import { gaussian, test } from "./random_utils.js";
import { logger } from "./utils.js";
import Family from "./models/family.js";
import RarityTable from "./tables/rarity_table.js";
const DECEASED = "deceased";
const LIFE_EXPECTANCY = [0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 100, 100];
const COPY_PROPS = [
"profession",
"honorific",
"description",
"degree",
"traits",
"clothing",
"possessions",
];
const COUPLE_TYPE = new RarityTable();
COUPLE_TYPE.add("common", "married");
COUPLE_TYPE.add("uncommon", "couple");
COUPLE_TYPE.add("rare", "separated");
COUPLE_TYPE.add("rare", "divorced");
class FamilyTree {
constructor() {
this.women = [];
this.all = [];
this.generation = 1;
}
addPerson(person) {
if (!person.male) {
this.women.push(person);
}
this.all.push(person);
}
mayHaveChild(woman) {
let youngestChildAge = ageOfYoungestChild(woman) || 4;
if (test(youngestChildAge * 5)) {
this.birth(woman);
}
}
birth(woman) {
const child = createChild(woman);
woman.children = woman.children ?? [];
woman.children.push(child);
this.addPerson(child);
this.generation = Math.max(this.generation, child.gen);
}
partner(person) {
person.partner = createPartner(person);
person.relationship = COUPLE_TYPE.get();
if (!person.male && test(80)) {
person.name.family = person.partner.name.family;
}
this.addPerson(person.partner);
}
mayDie(person) {
if (test(chanceToDie(person))) {
this.death(person);
}
}
death(person) {
person.status = DECEASED;
person.description.push(`Died at age ${person.age}`);
}
nextYear() {
this.living.forEach((person) => {
// what we would really like to say is name (died at age N);
person.age += 1;
// this is a child who now gains adult skills
if (person.age === 18) {
// not sure where to get this...we need the prestige of a parent's profession
const postProfession = findRelatedProfession({ name: person.parentProfession });
let character = createCharacter({ gender: person.gender, age: person.age, postProfession });
COPY_PROPS.forEach((prop) => (person[prop] = character[prop]));
}
});
}
get living() {
return this.all.filter((ch) => ch.status !== DECEASED);
}
get childBearers() {
return this.women.filter((ch) => {
return (
ch.status !== DECEASED &&
ch.partner &&
ch.partner?.status !== DECEASED &&
ch.age >= 18 &&
ch.age <= 40
);
});
}
get fallingInLove() {
return this.all.filter(
(ch) => !ch.partner && ch.status !== DECEASED && ch.age === ch.meetSomeone,
);
}
}
function createPartner(person) {
const gender = person.male ? "female" : "male";
const age = ageOfPartner(person);
const postProfession = findRelatedProfession({ name: person.profession });
const partner = createCharacter({ gender, age, postProfession, possessions: "None" });
partner.gen = person.gen;
return partner;
}
function createChild(woman) {
const child = createCharacter({
name: createCharacterName({
family: woman.partner.name.family,
heritage: test(50) ? woman.heritage : woman.partner.heritage,
}),
possessions: "None",
isChild: true,
});
child.parentProfession = woman.profession;
child.age = 0; // actually start at 0, isChild picks an age
child.gen = woman.gen + 1;
if (test(70)) {
child.meetSomeone = ageOfMeetingPartner();
}
return child;
}
function chanceToDie(ch) {
return Math.ceil(LIFE_EXPECTANCY[Math.floor(ch.age / 10)]);
}
function ageOfPartner(person) {
let age = person.age + gaussian(1, 0, true);
return age < 18 ? 18 : age;
}
function ageOfYoungestChild(woman) {
if (woman.children?.length > 0) {
return woman.children[woman.children.length - 1].age;
}
return 0;
}
function ageOfMeetingPartner() {
return gaussian(3, 26);
}
// Queer couples and adopted children. Other unusual household
// arrangements as listed in the relationships file...though ultimately, we may need
// a createHousehold to deal with that, building on the relationship and family
// generator. Then we need a way to place these in the world.
/**
* Creates a multi-generational family with characters, relationships, and a shared family name.
* The family tree simulates births, deaths, and partnerships over time. A generation is counted
* when a child is born into the tree.
*
* @param {Object} [options={}] - Family creation options
* @param {number} [options.generations=1] - Number of generations to simulate. Higher values produce
* larger families with grandchildren and great-grandchildren.
* @returns {Family} A Family instance rooted at the founding mother, containing the full family tree
*/
export function createFamily({ generations = 1 } = {}) {
logger.start("createFamily", { generations });
do {
var family = createOneFamily({ generations });
} while (family.generation !== generations);
return logger.end(family);
}
function createOneFamily({ generations = 1 } = {}) {
let yearsPastThreshold = gaussian(4, 14);
let safetyLoops = gaussian(3, 80);
const tree = new FamilyTree();
const mother = createCharacter({ gender: "female", age: 17 });
mother.gen = 1;
tree.addPerson(mother);
tree.partner(mother);
while (yearsPastThreshold > 0 && safetyLoops > 0) {
safetyLoops--;
if (tree.generation >= generations) {
yearsPastThreshold--;
}
tree.nextYear();
tree.fallingInLove.forEach((person) => tree.partner(person));
if (yearsPastThreshold > 4) {
tree.childBearers.forEach((person) => tree.mayHaveChild(person));
}
tree.living.forEach(tree.mayDie.bind(tree));
}
return new Family({ parent: mother, generation: tree.generation });
}