create_relationship.js

import { createAdultAge, createCharacter, createHeritage } from "./create_character.js";
import { findRelatedProfession } from "./create_profession.js";
import { logger } from "./utils.js";
import { random, test } from "./random_utils.js";
import { titleCase } from "./string_utils.js";
import RarityTable from "./tables/rarity_table.js";
import Relationship from "./models/relationship.js";

// Here are the most common (50) family/households, which I've boiled down a bit. There's more
// diversity here than captured in these generators (although they are a pretty good start).
// 2 siblings
// 1-4 friends
// child, parent
// grandparent, grandkid
// householder, 1-2 parents
// householder, friend or partner
// householder, friend or partner, non-relative
// householder, kid, parent
// householder, non-relative
// married couple
// married couple, 1-2 grandkids
// married couple, 1-2 kids, 1-2 grandkids
// married couple, 1-2 kids, parent-in-law
// married couple, 1-6 kids
// married couple, 2 kids, relative
// married couple, grandparent
// married couple, kid, grandkid
// married couple, kid, non-relative
// married couple, kid, parent
// married couple, non-relative
// married couple, relative
// married couple, siblings
// parent, 1-3 kids, friend or partner
// parent, 1-4 kids
// parent, 2 kids, grandkid
// parent, 4 kids, friend or partner
// parent, kid, 1-2 grandkids
// parent, kid, grandparent
// parent, kid, non-relative
// parent, kid, sibling
// parent, kid, stepkid

// g = gender
// a = age difference
// r = relationship pairs
// ln = older or same gen relationship, % chance younger/peer has the same last name
// n = name (added below)
const relationships = {
  grandmother: { g: "female", a: 0, r: ["grandson", "granddaughter"], ln: 25 },
  grandfather: { g: "male", a: 0, r: ["grandson", "granddaughter"], ln: 25 },
  aunt: { g: "female", a: 1, r: ["niece", "nephew"], ln: 50 },
  uncle: { g: "male", a: 1, r: ["niece", "nephew"], ln: 50 },
  mother: { g: "female", a: 1, r: ["son", "daughter"], ln: 100 },
  father: { g: "male", a: 1, r: ["son", "daughter"], ln: 100 },
  brother: { g: "male", a: 2, r: ["sister", "brother"], ln: 100 },
  sister: { g: "female", a: 2, r: ["sister", "brother"], ln: 100 },
  cousin: { a: 2, r: ["cousin"], ln: 25 },
  niece: { g: "female", a: 2, r: ["aunt", "uncle"] },
  nephew: { g: "male", a: 2, r: ["aunt", "uncle"] },
  son: { g: "male", a: 2, r: ["mother", "father"] },
  daughter: { g: "female", a: 2, r: ["mother", "father"] },
  grandson: { g: "male", a: 2, r: ["grandmother", "grandfather"] },
  granddaughter: { g: "female", a: 2, r: ["grandmother", "grandfather"] },
};
Object.keys(relationships).forEach((key) => (relationships[key].n = key));

// You only have to put the older terms in this table because if you're using
// it, you're randomizing, and the younger will be selected from the older
const rtable = new RarityTable()
  .add("common", relationships.mother)
  .add("common", relationships.father)
  .add("common", relationships.brother)
  .add("common", relationships.sister)
  .add("uncommon", relationships.aunt)
  .add("uncommon", relationships.uncle)
  .add("uncommon", relationships.cousin)
  .add("rare", relationships.grandmother)
  .add("rare", relationships.grandfather);

const olderRelNames = Object.values(relationships)
  .filter((r) => r.ln)
  .map((r) => titleCase(r.n));

export function getRelationships() {
  return olderRelNames.sort();
}

/**
 * Creates a pair of related characters (e.g. mother and son, aunt and nephew) with appropriate
 * age differences, shared or independent heritage, and related professions.
 *
 * @param {Object} [options={}] - Relationship creation options
 * @param {boolean} [options.equip=true] - Whether to equip both characters with traveling possessions.
 * @param {string} [options.relation] - The relationship type for the older character, selected from
 *    the values returned by `getRelationships()` (e.g. "Mother", "Father", "Brother"). If not
 *    provided, randomly selected by rarity.
 * @param {string} [options.familyName=null] - An optional family name to assign to the older character.
 *    The younger character may inherit it based on the relationship type.
 * @returns {Relationship} A Relationship instance containing both characters and their relationship label
 */
export function createRelationship({
  equip = true,
  relation = rtable.get().n,
  familyName = null,
} = {}) {
  logger.start("createRelationship", { equip, relation, familyName });
  let rel = relationships[relation.toLowerCase()];
  let reverse = relationships[random(rel.r)];
  let youngerRel = rel.ln ? reverse : rel;
  let olderRel = rel.ln ? rel : reverse;
  let possessions = equip ? "Traveling" : "None";

  let ageDiff = Math.abs(olderRel.a - youngerRel.a);
  let youngerAge, olderAge;
  do {
    youngerAge = createAdultAge();
    olderAge = createAdultAge();
    for (let i = 0; i < ageDiff; i++) {
      olderAge += createAdultAge();
    }
  } while (ageDiff > 0 && olderAge - youngerAge < 18 * ageDiff);

  let older = createCharacter({
    age: olderAge,
    gender: olderRel.g,
    possessions,
  });
  let younger = createCharacter({
    age: youngerAge,
    // not sure this accords with American theories about heritage...
    heritage: test(olderRel.ln) ? older.heritage : createHeritage(),
    gender: youngerRel.g,
    profession: findRelatedProfession({ name: older.profession }),
    possessions,
  });

  let relName = olderRel === youngerRel ? `${olderRel.n}s` : `${olderRel.n} and ${youngerRel.n}`;

  if (familyName) {
    older.name.family = familyName;
  }
  if (test(olderRel.ln)) {
    younger.name.family = older.name.family;
  }
  return logger.end(new Relationship({ older, younger, relName }));
}