code:
type NpcStatus =
  | "citizen"
  | "slave"
  | "home slave"
  | "accomplice"
  | "pet"
  | "servant"
  | "lover";
type NpcEventType = "demand" | "ordinary";
interface NpcValue {
  obedience: number;
  obedienceRatio: string;
  lust: number;
  lustRatio: string;
  pussy: number;
  pussyRatio: string;
  anus: number;
  anusRatio: string;
  mouth: number;
  mouthRatio: string;
  freedomWish?: number;
  virginType?: string;
  virginBonus?: number;
  total?: number;
}
abstract class Npc {
  uid: Uid;
  name: string;
  age: number;
  sex: Sex;
  gender: Gender;
  hasPussy: boolean;
  hasPenis: boolean;
  hasBoobs: boolean;
  lactating: boolean;
  lubricatedAss: boolean;
  lubricatedPussy: boolean;
  pussyTraining: number = 0;
  anusTraining: number = 0;
  mouthTraining: number = 0;
  penisVirgin: boolean = true;
  vaginaVirgin: boolean = true;
  analVirgin: boolean = true;
  mouthVirgin: boolean = true; //Due to a personal error only slaves with previous personal experience have this at false.
  assSpermAmount: number = 0;
  pussySpermAmount: number = 0;
  bodySpermAmount: number = 0;
  faceSpermAmount: number = 0;
  children: Uid[] = [];
  mom: Uid = null;
  dad: Uid = null;
  aroused: boolean = false;
  hunger: number = 0;
  fear: number = 50;
  love: number = 0;
  obedience: number = 25;
  lust: number = 0;
  freedomWish: number = 75;
  genitals: AllGenitals;
  status: NpcStatus = "citizen";
  //Name of the last event assigned to this NPC
  event: string;
  //Type of the last event
  eventType: NpcEventType;
  //This property is not used anymore, it was to identify a slave using the index in the slave array.
  //Changes in the array caused problems easily so it was replaced with the uid property
  index: undefined;
  //achievements can be used to keep track of what the npc has experienced
  achievements: string[] = [];
  //for long-lasting punishments
  punishments: string[] = [];
  //Where this NPC is located in the house or in the world, try to match up with the scenery name.
  location: string = "unknown";
  inventory: Inventory;
  getInventory(npc?: Npc): Inventory {
    if (!npc) npc = Variables().npc;
    return (npc.inventory = new Inventory(npc.inventory));
  }
  hasAchievement(achievement: string, npc?: Npc): boolean {
    if (!npc) npc = Variables().npc;
    return npc.achievements.includes(achievement);
  }
  hasAnyAchievement(achievements: string[], npc?: Npc) {
    if (!npc) npc = Variables().npc;
    return npc.achievements.includesAny(achievements);
  }
  hasAllAchievements(achievements: string[], npc?: Npc) {
    if (!npc) npc = Variables().npc;
    return npc.achievements.includesAll(achievements);
  }
  setAchievement(achievement: string, npc?: Npc): void {
    if (!npc) npc = Variables().npc;
    if (!this.hasAchievement(achievement, npc))
      npc.achievements.push(achievement);
  }
  removeAchievement(achievement: string, npc?: Npc): void {
    if (!npc) npc = Variables().npc;
    npc.achievements.delete(achievement);
  }
  getValue(npc?: Npc): NpcValue {
    if (!npc) npc = Variables().npc;
    const maxNonVirgin = 1500;
    const maxFwPenalty = -0.75; //Maximum freedom wish penalty 75%
    let calcValue = (stat: number, ratio: number) =>
      (maxNonVirgin * (stat * ratio)) / 100;
    let value: NpcValue = npc.hasPussy
      ? {
          obedience: calcValue(npc.obedience, (0.27 * npc.obedience) / 100),
          obedienceRatio: "(27%)",
          lust: calcValue(npc.lust, 0.25),
          lustRatio: "(25%)",
          pussy: calcValue(npc.pussyTraining, 0.19),
          pussyRatio: "(19%)",
          anus: calcValue(npc.anusTraining, 0.16),
          anusRatio: "(16%)",
          mouth: calcValue(npc.mouthTraining, 0.13),
          mouthRatio: "(13%)",
        }
      : {
          obedience: calcValue(npc.obedience, (0.3 * npc.obedience) / 100),
          obedienceRatio: "(30%)",
          lust: calcValue(npc.lust, 0.27),
          lustRatio: "(27%)",
          pussy: 0,
          pussyRatio: "(0%)",
          anus: calcValue(npc.anusTraining, 0.23),
          anusRatio: "(23%)",
          mouth: calcValue(npc.mouthTraining, 0.2),
          mouthRatio: "(20%)",
        };
    value.total =
      value.obedience + value.lust + value.pussy + value.anus + value.mouth;
    value.freedomWish = (value.total * maxFwPenalty * npc.freedomWish) / 100;
    if (npc.hasPussy) {
      if (npc.vaginaVirgin) value.virginType = "virgin " + npc.genitals.female;
    } else if (npc.analVirgin) value.virginType = "anal virginity";
    value.virginBonus = value.virginType ? Math.max(5, value.total * 0.2) : 0;
    value.total += value.freedomWish + value.virginBonus;
    return value;
  }
  setStatus(status: NpcStatus, npc?: Npc): void {
    if (!npc) npc = Variables().npc;
    switch (status) {
      case "slave":
        npc.location = "basement";
        break;
      case "home slave":
        npc.location = "mainRoom";
        break;
    }
    npc.status = status;
  }
  capture(npc: Npc): void {
    this.setStatus("slave", npc);
    Variables().slaves.push(npc);
  }
  static updateLocations(currentDate: Date): void {
    const variables = Variables();
    if (!currentDate) currentDate = variables.now.date;
    //const sleepTime = window.Now.isBetween("10:00 PM", "7:00 AM", currentDate);
    const homeSpaces = variables.player.home.spaces.filter(
      (space: string) => space != "basement" && !space.startsWith("tort")
    );
    const baseSeed = currentDate.getTime() - 1649048400000; //Current date/time minus the start of the game
    variables.slaves.forEach((slave: Person) => {
      switch (slave.status) {
        case "home slave":
        case "lover":
          slave.location = PseudoRandom.either(
            PseudoRandom.getSeed(baseSeed, slave.age, slave.name),
            homeSpaces
          );
          break;
        case "servant":
          if (variables.settings.cook.npc != slave.uid) break;
          let currentDate = window.Now.getCurrentDate();
          for (const feedTimeString of variables.settings.cook.feedTimes) {
            const currentTimeStamp = currentDate.getTime();
            const feedTimeStamp = window.Now.dateFromTimeString(
              feedTimeString,
              currentDate
            ).getTime();
            if (
              currentTimeStamp >= feedTimeStamp - 60 * 60 * 1000 &&
              currentTimeStamp <= feedTimeStamp
            ) {
              currentDate = null;
              slave.location = "kitchen";
              break;
            }
          }
          if (currentDate)
            slave.location = PseudoRandom.either(
              PseudoRandom.getSeed(baseSeed, slave.age, slave.name),
              homeSpaces.filter((space: string) => space != "kitchen")
            );
          break;
      }
    });
  }
  get(uid: Uid): Npc {
    return Variables().slaves.firstOrDefault(
      (slave: Person) => slave.uid == uid
    );
  }
  getMinRequirementsSentence(
    requirements: Record<string, number>,
    npc?: Npc,
    showCurrent?: boolean
  ): string {
    if (!npc) npc = Variables().npc;
    let entries = [];
    for (let requirement in requirements)
      if (npc[requirement] < requirements[requirement])
        entries.push(
          requirement.beautifyStat() +
            " " +
            (showCurrent ? npc[requirement] + "/" : "") +
            requirements[requirement]
        );
    return entries.join(", ");
  }
}
type AnimalSpecies = "dog" | "cat" | "rabbit" | "horse" | "pig" | "cow";
type RoughSize = "tiny" | "small" | "normal" | "big" | "very big";
class Animal extends Npc {
  species: AnimalSpecies;
  roughSize?: RoughSize;
}
const genderList: Gender[] = ["boy", "girl", "nb"];
class Person extends Npc {
  version: number = 2;
  title: string;
  pronoun: string;
  genPronoun: string;
  GenPronoun: string;
  possessive: string;
  Possessive: string;
  hairColor: string;
  hairLength: string;
  hairStyle: string;
  eyeColor: string;
  skin: string;
  haveClothes: boolean = true;
  uniqueness: PersonUniqueness;
  generate(gen?: PersonGeneration): Person {
    if (gen === undefined) gen = new PersonGeneration();
    let person = new Person();
    person.sex =
      Math.random() * 100 + 1 < gen.femalePercentage ? "female" : "male";
    person.sex =
      Math.random() * 100 + 1 < gen.hermPercentage ? "herm" : person.sex;
    person.gender =
      person.sex == "male"
        ? "boy"
        : person.sex == "female"
        ? "girl"
        : genderList.random();
    person.title =
      person.sex == "male" ? "boy" : person.sex == "female" ? "girl" : "child";
    person.pronoun =
      person.gender == "boy" ? "him" : person.gender == "girl" ? "her" : "them";
    person.GenPronoun =
      person.gender == "boy" ? "He" : person.gender == "girl" ? "She" : "They";
    person.genPronoun =
      person.gender == "boy" ? "he" : person.gender == "girl" ? "she" : "they";
    person.possessive =
      person.gender == "boy"
        ? "his"
        : person.gender == "girl"
        ? "her"
        : "their";
    person.Possessive =
      person.gender == "boy"
        ? "His"
        : person.gender == "girl"
        ? "Her"
        : "Their";
    person.hasPussy = person.sex == "female" || person.sex == "herm";
    person.hasPenis = person.sex == "male" || person.sex == "herm";
    let genGen: GenderGeneration =
      person.sex == "male"
        ? gen.males
        : person.sex == "female"
        ? gen.females
        : gen.herms;
    person.age =
      Math.floor(Math.random() * (genGen.toAge - genGen.fromAge)) +
      genGen.fromAge;
    person.hasBoobs = (person.sex == "female" || person.sex == "herm") && person.age >= 13;
    if (person.age < 6)
      person.freedomWish -= ((6 - person.age) / 6) * person.freedomWish;
    person.genitals = {
      male: person.sex == "male" || person.sex == "herm" ? "dick" : null,
      female:
        person.sex == "female" || person.sex == "herm"
          ? person.age < 15
            ? "cunny"
            : "pussy"
          : null,
      all:
        person.sex == "male"
          ? "dick"
          : person.sex == "female"
          ? person.age < 15
            ? "cunny"
            : "pussy"
          : person.age < 15
          ? "dick and cunny"
          : "dick and pussy",
    };
    person.name = (person.gender != "boy" ? femaleNames : maleNames).random();
    person.skin = gen.skins.random();
    person.hairColor = gen.hairColors.random();
    person.hairLength =
      person.age == 0
        ? "short"
        : person.age == 1
        ? ["short", "medium"].random()
        : ["short", "medium", "long"].random();
    let hairStyles = gen.hairStyles;
    if (person.gender == "boy")
      hairStyles.delete("pig tails", "twin tails", "ponytail");
    person.hairStyle = hairStyles.random();
    person.eyeColor = gen.eyeColors.random();
    person.uid = getUid();
    PersonUniqueness.applyRandom(person);
    return person;
  }
  getWandering(): Person[] {
    let variables = Variables();
    return variables.slaves.filter(
      (slave: Person) => slave.location == variables.scenery
    );
  }
  getPersonalityDescription(uniqueness: PersonUniqueness): string {
    return Object.keys(uniqueness)
      .filter((keyName) => typeof uniqueness[keyName] == "boolean")
      .join(", ");
  }
  getHomePersonName(word: string, npc?: Npc): string {
    if (!npc) npc = Variables().npc;
    if (word == "sibling") word = npc.sex == "male" ? "bro" : "sis";
    if (npc.age < 5) {
      switch (word) {
        case "uncle":
          return "uncle";
        case "dad":
          return "dada";
        case "mom":
          return "mama";
      }
      if (word.includes(" ")) {
        if (word.includes("girl")) return "a girl";
        if (word.includes("boy")) return "a boy";
      }
      return word;
    }
    if (npc.age > 10) {
      switch (word) {
        case "bro":
          word = "brother";
          break;
        case "sis":
          word = "sister";
          break;
      }
      if (!word.includes(" ")) word = "my " + word;
    } else {
      switch (word) {
        case "dad":
          word = "daddy";
          break;
        case "mom":
          word = "mommy";
          break;
      }
    }
    return word;
  }
  getHomePersonSex(word: string): Sex {
    switch (word) {
      case "dad":
      case "bro":
      case "uncle":
      case "pa":
        return "male";
      case "mom":
      case "sis":
      case "aunt":
      case "ma":
        return "female";
    }
    return word.includes("boy") ? "male" : "female";
  }
  getShortDescription(person?: Person, addTitle?: boolean): string {
    if (!person) person = Variables().npc;
    return `${person.name} (${person.age} y.o. ${
      addTitle ? person.title + " " : ""
    }${person.hairColor} hair)`;
  }
  getLongDescription(person?: Person): string {
    if (!person) person = Variables().npc;
    return `${person.age} year old ${person.skin} ${person.title} with ${person.hairLength} ${person.hairStyle} ${person.hairColor} hair and ${person.eyeColor} eyes`;
  }
}
interface GenderGeneration {
  fromAge: number;
  toAge: number;
}
class PersonGeneration {
  females: GenderGeneration = {
    fromAge: 1,
    toAge: 15,
  };
  males: GenderGeneration = {
    fromAge: 1,
    toAge: 15,
  };
  herms: GenderGeneration = {
    fromAge: 1,
    toAge: 15,
  };
  femalePercentage: number = 50;
  hermPercentage: number = 0;
  hairStyles = [
    "curly",
    "wavey",
    "straight",
    "emo bangs",
    "fauxhawkian",
    "front spikes",
    "wavy side part",
    "asymmetrical",
    "ponytail",
    "twin tails",
    "pig tails",
  ];
  eyeColors = ["green", "blue", "brown", "hazel"];
  hairColors = [
    "black",
    "dark brown",
    "brown",
    "light brown",
    "dirty blonde",
    "blonde",
    "red",
    "auburn",
    "midnight blue",
    "rainbow",
    "pale pink",
    "hot pink",
    "burgundy",
    "royal purple",
    "violet",
    "indigo",
    "blue",
  ];
  skins = ["tan", "brown", "black", "white", "pale", "olive"];
  load(definition: Object | string) {
    if (typeof definition == "string") definition = JSON.parse(definition);
    Object.assign(this, definition);
  }
}
const maleNames: string[] = [
  "Liam",
  "Noah",
  "William",
  "James",
  "Oliver",
  "Benjamin",
  "Elijah",
  "Lucas",
  "Mason",
  "Logan",
  "Alexander",
  "Ethan",
  "Jacob",
  "Michael",
  "Daniel",
  "Henry",
  "Jackson",
  "Sebastian",
  "Aiden",
  "Matthew",
  "Samuel",
  "David",
  "Joseph",
  "Carter",
  "Owen",
  "Wyatt",
  "John",
  "Jack",
  "Luke",
  "Jayden",
  "Dylan",
  "Grayson",
  "Levi",
  "Isaac",
  "Gabriel",
  "Julian",
  "Mateo",
  "Anthony",
  "Jaxon",
  "Lincoln",
  "Joshua",
  "Christopher",
  "Andrew",
  "Theodore",
  "Caleb",
  "Ryan",
  "Asher",
  "Nathan",
  "Thomas",
  "Leo",
  "Isaiah",
  "Charles",
  "Josiah",
  "Hudson",
  "Christian",
  "Hunter",
  "Connor",
  "Eli",
  "Ezra",
  "Aaron",
  "Landon",
  "Adrian",
  "Jonathan",
  "Nolan",
  "Jeremiah",
  "Easton",
  "Elias",
  "Colton",
  "Cameron",
  "Carson",
  "Robert",
  "Angel",
  "Maverick",
  "Nicholas",
  "Dominic",
  "Jaxson",
  "Greyson",
  "Adam",
  "Ian",
  "Austin",
  "Santiago",
  "Jordan",
  "Cooper",
  "Brayden",
  "Roman",
  "Evan",
  "Ezekiel",
  "Xavier",
  "Jose",
  "Jace",
  "Jameson",
  "Leonardo",
  "Bryson",
  "Axel",
  "Everett",
  "Parker",
  "Kayden",
  "Miles",
  "Sawyer",
  "Jason",
  "Declan",
  "Weston",
  "Micah",
  "Ayden",
  "Wesley",
  "Luca",
  "Vincent",
  "Damian",
  "Zachary",
  "Silas",
  "Gavin",
  "Chase",
  "Kai",
  "Emmett",
  "Harrison",
  "Nathaniel",
  "Kingston",
  "Cole",
  "Tyler",
  "Bennett",
  "Bentley",
  "Ryker",
  "Tristan",
  "Brandon",
  "Kevin",
  "Luis",
  "George",
  "Ashton",
  "Rowan",
  "Braxton",
  "Ryder",
  "Gael",
  "Ivan",
  "Diego",
  "Maxwell",
  "Max",
  "Carlos",
  "Kaiden",
  "Juan",
  "Maddox",
  "Justin",
  "Waylon",
  "Calvin",
  "Giovanni",
  "Jonah",
  "Abel",
  "Jayce",
  "Jesus",
  "Amir",
  "King",
  "Beau",
  "Camden",
  "Alex",
  "Jasper",
  "Malachi",
  "Brody",
  "Jude",
  "Blake",
  "Emmanuel",
  "Eric",
  "Brooks",
  "Elliot",
  "Antonio",
  "Abraham",
  "Timothy",
  "Finn",
  "Rhett",
  "Elliott",
  "Edward",
  "August",
  "Xander",
  "Alan",
  "Dean",
  "Lorenzo",
  "Bryce",
  "Karter",
  "Victor",
  "Milo",
  "Miguel",
  "Hayden",
  "Graham",
  "Grant",
  "Zion",
  "Tucker",
  "Jesse",
  "Zayden",
  "Joel",
  "Richard",
  "Patrick",
  "Emiliano",
  "Avery",
  "Nicolas",
  "Brantley",
  "Dawson",
  "Myles",
  "Matteo",
  "River",
  "Steven",
  "Thiago",
  "Zane",
];
const femaleNames: string[] = [
  "Emma",
  "Olivia",
  "Ava",
  "Isabella",
  "Sophia",
  "Charlotte",
  "Mia",
  "Amelia",
  "Harper",
  "Evelyn",
  "Abigail",
  "Emily",
  "Elizabeth",
  "Mila",
  "Ella",
  "Avery",
  "Sofia",
  "Camila",
  "Aria",
  "Scarlett",
  "Victoria",
  "Madison",
  "Luna",
  "Grace",
  "Chloe",
  "Penelope",
  "Layla",
  "Riley",
  "Zoey",
  "Nora",
  "Lily",
  "Eleanor",
  "Hannah",
  "Lillian",
  "Addison",
  "Aubrey",
  "Ellie",
  "Stella",
  "Natalie",
  "Zoe",
  "Leah",
  "Hazel",
  "Violet",
  "Aurora",
  "Savannah",
  "Audrey",
  "Brooklyn",
  "Bella",
  "Claire",
  "Skylar",
  "Lucy",
  "Paisley",
  "Everly",
  "Anna",
  "Caroline",
  "Nova",
  "Genesis",
  "Emilia",
  "Kennedy",
  "Samantha",
  "Maya",
  "Willow",
  "Kinsley",
  "Naomi",
  "Aaliyah",
  "Elena",
  "Sarah",
  "Ariana",
  "Allison",
  "Gabriella",
  "Alice",
  "Madelyn",
  "Cora",
  "Ruby",
  "Eva",
  "Serenity",
  "Autumn",
  "Adeline",
  "Hailey",
  "Gianna",
  "Valentina",
  "Isla",
  "Eliana",
  "Quinn",
  "Nevaeh",
  "Ivy",
  "Sadie",
  "Piper",
  "Lydia",
  "Alexa",
  "Josephine",
  "Emery",
  "Julia",
  "Delilah",
  "Arianna",
  "Vivian",
  "Kaylee",
  "Sophie",
  "Brielle",
  "Madeline",
  "Peyton",
  "Rylee",
  "Clara",
  "Hadley",
  "Melanie",
  "Mackenzie",
  "Reagan",
  "Adalynn",
  "Liliana",
  "Aubree",
  "Jade",
  "Katherine",
  "Isabelle",
  "Natalia",
  "Raelynn",
  "Maria",
  "Athena",
  "Ximena",
  "Arya",
  "Leilani",
  "Taylor",
  "Faith",
  "Rose",
  "Kylie",
  "Alexandra",
  "Mary",
  "Margaret",
  "Lyla",
  "Ashley",
  "Amaya",
  "Eliza",
  "Brianna",
  "Bailey",
  "Andrea",
  "Khloe",
  "Jasmine",
  "Melody",
  "Iris",
  "Isabel",
  "Norah",
  "Annabelle",
  "Valeria",
  "Emerson",
  "Adalyn",
  "Ryleigh",
  "Eden",
  "Emersyn",
  "Anastasia",
  "Kayla",
  "Alyssa",
  "Juliana",
  "Charlie",
  "Esther",
  "Ariel",
  "Cecilia",
  "Valerie",
  "Alina",
  "Molly",
  "Reese",
  "Aliyah",
  "Lilly",
  "Parker",
  "Finley",
  "Morgan",
  "Sydney",
  "Jordyn",
  "Eloise",
  "Trinity",
  "Daisy",
  "Kimberly",
  "Lauren",
  "Genevieve",
  "Sara",
  "Arabella",
  "Harmony",
  "Elise",
  "Remi",
  "Teagan",
  "Alexis",
  "London",
  "Sloane",
  "Laila",
  "Lucia",
  "Diana",
  "Juliette",
  "Sienna",
  "Elliana",
  "Londyn",
  "Ayla",
  "Callie",
  "Gracie",
  "Josie",
  "Amara",
  "Jocelyn",
  "Daniela",
  "Everleigh",
  "Mya",
  "Rachel",
  "Summer",
  "Alana",
];
type PersonGrowthStage =
  | "baby"
  | "toddler"
  | "junior"
  | "elementary"
  | "pubescent"
  | "postPubescent";
interface PersonGrowthStageRange {
  from: number;
  to: number;
}
const GrowthStageAge: Record<PersonGrowthStage, PersonGrowthStageRange> = {
  baby: {
    from: 0,
    to: 0,
  },
  toddler: {
    from: 1,
    to: 4,
  },
  junior: {
    from: 5,
    to: 7,
  },
  elementary: {
    from: 8,
    to: 11,
  },
  pubescent: {
    from: 12,
    to: 15,
  },
  postPubescent: {
    from: 16,
    to: 25,
  },
};
class PersonUniqueness {
  name: string; //Personality name or personality role in home
  curious?: boolean; //Likes to learn new things, gives attention to others, high empathy
  naughty?: boolean; //Not easily disgusted, interested in feeling good mutually, open to every kink like incest or bestiality
  energetic?: boolean; //Optimistic, brave, takes initiative, wants to get attention, can become sadist
  shy?: boolean; //Not very talkative, tendency to blush, secretive, can become masochist
  diligent?: boolean; //Knowledgeable, does not hesitate, strict, does what it needs to be done
  homePersons?: PersonUniqueness[]; //Personalities of other persons at home (family, guardians, roommates, etc...)
  homeOtherNpc?: Npc[]; //Other NPCs in this person home that are not persons (pets basically)
  apply?: (person: Person) => void = () => {}; //Function to modify the person that this uniqueness is applied, if necessary (remove virginity, change stat, etc...)
  appearingChance?: number = 50; //Mathematical weight, the higher the default value is the rarer will be for the lower values
  ageRange?: PersonGrowthStageRange;
  constructor(prototype: PersonUniqueness) {
    Object.assign(this, prototype);
  }
  static applyRandom(person: Person, apply = true): PersonUniqueness {
    let randomNumber = PseudoRandom.getFromRange(
      PseudoRandom.getSeed(person.age, person.name),
      0,
      personUniquenessPresets.reduce(
        (acc, preset) => acc + preset.appearingChance,
        0
      )
    );
    for (const preset of personUniquenessPresets) {
      if (randomNumber < preset.appearingChance) {
        person.uniqueness = preset;
        if (apply) preset.apply(person);
        console.info(preset);
        return;
      }
      randomNumber -= preset.appearingChance;
    }
  }
}
const personUniquenessPresets: PersonUniqueness[] = [
  new PersonUniqueness({
    name: "bottom",
    curious: true,
    shy: true,
    homePersons: [
      {
        name: "mom",
        curious: true,
      },
      {
        name: "dad",
        diligent: true,
      },
    ],
  }),
  new PersonUniqueness({
    name: "jumpy",
    curious: true,
    naughty: true,
    energetic: true,
    homePersons: [
      {
        name: "mom",
        curious: true,
        diligent: true,
      },
      {
        name: "dad",
        shy: true,
      },
      {
        name: "sis",
        shy: true,
      },
    ],
  }),
  new PersonUniqueness({
    name: "experiencedShy",
    shy: true,
    homePersons: [
      {
        name: "dad",
        shy: true,
        curious: true,
        naughty: true,
      },
      {
        name: "mom",
        curious: true,
        diligent: true,
      },
      {
        name: "bro",
        naughty: true,
        energetic: true,
      },
    ],
    apply(kid) {
      kid.mouthVirgin = false;
      kid.mouthTraining = Math.min(100, kid.age * 9);
      if (!kid.hasPussy || kid.age < 6) {
        kid.analVirgin = false;
        kid.anusTraining = 60;
      } else {
        kid.vaginaVirgin = false;
        kid.pussyTraining = 60;
      }
    },
    appearingChance: 13,
  }),
  new PersonUniqueness({
    name: "Carrabina",
    curious: true,
    naughty: true,
    shy: true,
    diligent: true,
    homePersons: [
      {
        name: "mom",
        curious: true,
      },
      {
        name: "dad",
        diligent: true,
        energetic: true,
      },
      {
        name: "bro",
        naughty: true,
        curious: true,
        energetic: true,
      },
    ],
    apply(kid) {
      if (kid.age > 4) {
        kid.lust = 40;
        if (kid.age > 7) {
          kid.mouthVirgin = false;
          kid.mouthTraining = Math.min(100, (kid.age - 7) * 34);
          kid.lust = 60;
          if (kid.age > 8) {
            kid.analVirgin = false;
            kid.anusTraining = Math.min(80, (kid.age - 8) * 60);
          }
        }
      }
    },
    appearingChance: 11,
  }),
  new PersonUniqueness({
    name: "experiencedOrphan",
    naughty: true,
    energetic: true,
    diligent: true,
    homePersons: [
      {
        name: "uncle",
        curious: true,
        naughty: true,
      },
      {
        name: "sibling",
        naughty: true,
        energetic: true,
      },
      {
        name: "uncle's girlfriend",
        curious: true,
        naughty: true,
      },
    ],
    homeOtherNpc: [
      {
        name: "Poppy",
        species: "rabbit",
        status: "pet",
      } as Animal,
    ],
    apply(kid) {
      if (kid.hasPussy) kid.vaginaVirgin = false;
      if (kid.hasPenis) kid.penisVirgin = false;
      if (kid.hasPussy) kid.pussyTraining = Math.min(100, kid.age * (100 / 5));
      if (kid.age > 4) kid.lust = 60;
      kid.mouthVirgin = false;
      kid.mouthTraining = 100;
      kid.anusTraining = 10;
    },
    appearingChance: 12,
  }),
  new PersonUniqueness({
    name: "top",
    curious: true,
    energetic: true,
    homePersons: [
      {
        name: "mom",
        shy: true,
      },
      {
        name: "dad",
        shy: true,
      },
      {
        name: "sis",
        curious: true,
        energetic: true,
      },
    ],
    appearingChance: 75,
  }),
  new PersonUniqueness({
    name: "spyX",
    energetic: true,
    homePersons: [
      {
        name: "pa",
        diligent: true,
      },
      {
        name: "ma",
        curious: true,
        energetic: true,
      },
    ],
    homeOtherNpc: [
      {
        name: "Bond",
        species: "dog",
        roughSize: "big",
      } as Animal,
    ],
    appearingChance: 15,
  }),
  new PersonUniqueness({
    name: "strictParents",
    energetic: true,
    diligent: true,
    curious: true,
    homePersons: [
      {
        name: "mom",
        diligent: true,
      },
      {
        name: "dad",
        diligent: true,
      },
    ],
  }),
];


code:
type NpcInteractionOptions = Record<string, NpcInteraction>;
type complexRequirement = {
  playerRequirements?: string[];
  npcRequirements?: string[];
  inventoryRequirements?: string[];
  locationRequirements?: string[];
  locationExclusions?: string[];
  settingsRequirements?: string[];
};
//Describes an option that once selected will display a passage and optionally affect the relationship between the player and an NPC
interface NpcInteraction {
  //If this function is specified it will be called in order to know if this interaction can be shown in a list of options
  canBeShown?: () => boolean;
  //A list of requirements that the player needs to comply in order to show this option
  playerRequirements?: string[];
  //A list of requirements that the npc needs to comply in order to show this option
  //Examples: lust>10, fear<=50, aroused, !analVirgin, love=100, status=servant
  npcRequirements?: string[];
  //A list of items that the player needs to have in order to show this option
  inventoryRequirements?: string[];
  //A list of items that the current location needs to comply (location name or items present in the location)
  locationRequirements?: string[];
  //A list of locations where this option can't be shown
  locationExclusions?: string[];
  //A list of requirements that the settings need to comply in order to show this option
  settingsRequirements?: string[];
  //A list of object containing the previous requirement variables, used when more than one combination of settings is acceptable.
  //Example: Allowing the lube option to show if anal is enabled and isn't lubricated, or if they have a pussy that is not lubricated.
  complexRequirements?: complexRequirement[];
  //The text in the option (before selecting the option).
  //if an emoji followed by a space is prepended it will be used as the emoji parameter in the keyOption or keyAction macro.
  //Sugarcube markup can be written on it.
  optionText: string;
  //The contents that will be shown as a passage once the option is selected.
  //The scenery of the last location will be shown.
  //Sugarcube markup can be written on it and it will be processed first (before any npc stat change)
  contents: string;
  //A list of npc stat changes that will be executed after the option is selected and the contents processed.
  //The changes will also be presented to the player in the passage.
  //Optionally, a function to generate the list can be provided.
  //"fear-10" will subtract exactly 10 to the npc's fear
  //"lust+10%" will add a 10% of the current value
  //"love%+40" will add a 40% of what is needed to reach 40 in love (fairmath) but at least will add 1
  //"+aroused" will set $npc.aroused to true and "-aroused" will set it to false
  npcStats?: string[] | ((npc: Npc) => string[]);
  //A list of player stat changes that will be executed after the option is selected and the contents processed.
  playerStats?: string[];
  //The next options to show in the passage after this option is selected.
  //Optionally, a function returning the options can be provided instead.
  //But be careful, the engine can lose the path to the interaction to show if the function does not return the option that leads to the current interaction and produce an error.
  //If that happens use altOptions and baseRoute instead.
  //After all those options the stop option will be shown unless specified not to (see below)
  next?: NpcInteractionOptions | (() => NpcInteractionOptions);
  //The text to show in the option to end the interaction with the NPC.
  //Once selected the player will be redirected to the passage where the interaction started with the openNpcInteraction macro.
  //If not specified the defaultStopOption in the parent interaction collection will be used.
  //If it's set to false the stop option will not be shown.
  stopOption?: string | false;
  //If set to true the npc's stats will be added to the passage.
  //If not specified it will show stats only when there's stats changes in the interaction.
  showNpcStats?: boolean | undefined;
  //The amount of minutes that will pass after selecting this option. It's also shown after the option text.
  minutesCost?: number;
  //A function to alter the options specified in the next field above in case you need a special set of options when this specific interaction is shown.
  altOptions?: (
    npc: Npc,
    current: NpcInteractionOptions
  ) => NpcInteractionOptions;
  //If specified the function is used to alter the minutesCost field when the option is selected. But the option will still show the minutes stated in minutesCost,
  altMinutes?: (current: number) => number;
  //If specified changes the base route to the options on this interaction.
  //For instance: If the function returns "slave.pushDown" and the options are named option1 and option2 the options will lead to the interactions "slave.pushDown.option1" and "slave.pushDown.option2"
  baseRoute?: (npc: Npc) => string;
  //If specified, forces to show or not this interaction if there's no options available for the player to select other than a "back" option or the stop option.
  //If not specified options without sub-options are only hidden if hideEmptyOptions on the parent collection is set to true.
  showIfEmpty?: boolean;
  //If set to true this interaction will not open in a new passage and execute the Sugarcube markup in the contents field instead once this option is selected.
  action?: boolean;
  //If specified, overrides timeIncreaseNpcHunger from the parent collection.
  timeIncreaseNpcHunger?: boolean | undefined;
  //If the option is not available it will still show, unlinked and with the specified string between brackets and parsed as Sugarcube markup.
  //A condition on the npc for showing the option can be prepended using '=>' as a separator.
  //Update 0.1.11.6: If no condition is specified canBeShown will be used as a condition instead, if it exists.
  //Example: "hasPussy=>Love $npc.love/50, Hunger $npc.hunger/30" to show "[Love 10/50, Hunger 20/30]" if NPC has a pussy.
  showDisabled?: string;
}
interface NpcInteractionCollection {
  //The first set of options in the collection (see NpcInteraction.next)
  options: NpcInteractionOptions | (() => NpcInteractionOptions);
  //The contents of the first passage.
  contents: string;
  //Every interaction in this interaction tree will have an option to exit the interaction and return to the passage that opened the first interaction.
  //This field indicates the text on that option. If not specified the text "🔙 Return" will be used.
  //If it's set to false, the stop option will never show unless specified in a sub-interaction.
  defaultStopOption?: string | false;
  //If this option is set to true all the interactions in this collection with minutesCost specified will increase NPC hunger unless it's overridden by the same field in that interaction.
  timeIncreaseNpcHunger?: boolean;
  //Hide all options in the entire collection that do not have any options to show other than a "back" option or the stop option.
  //Can be overridden by the showIfEmpty field in a specific interaction.
  hideEmptyOptions?: boolean;
  //Sugarcube markup action to do right before stopping the interaction.
  beforeStop?: string;
}
//Utility for properties that can be both a value and a function. If it's a function it calls the function with the provided arguments (if any) and returns the result.
let callOrGetItself = (valueOrFunction: any, ...args: any[]) =>
  typeof valueOrFunction != "function"
    ? valueOrFunction
    : valueOrFunction(...args);
//Redirects to the npcInteraction passage showing an interaction with a NPC.
//Usage: <<openNpcInteraction <interactionRoute>[ npcUid]>>
//interactionRoute is the name of the interaction collection (that must be added to window.Interactions array) followed by the name of the selected options
//npcUid is the target npc's UID and it's optional. If not specified the existing Npc in the $npc story variable will be used.
//Example: <<keyAction directPiston>><<openNpcInteraction 'slave.pushDown.strip.pushDickAnus.ram.fast' $slaves[0].uid>><</keyAction>>
Macro.add("openNpcInteraction", {
  handler: function () {
    let variables = Variables();
    if (SugarCube.State.passage != "npcInteraction")
      variables.returnPassage = SugarCube.State.passage;
    variables.npcInteractionRoute = this.args[0];
    if (this.args[1]) variables.npc = window.Person.get(this.args[1]);
    (<any>SugarCube.State).display("npcInteraction");
  },
});
const checkCondition = (objectName: string, condition: string): boolean => {
  let neg = "";
  if (condition[0] == "!") {
    neg = "!";
    condition = condition.slice(1);
  }
  return eval(
    neg +
      "variables()." +
      objectName +
      "." +
      condition
        .replace(/(\w[!=]=?=?)((?!true)(?!false)[^\d=].+)/, "$1'$2'")
        .replace(/([^><!=])=([^=])/, "$1==$2")
  );
};
const checkCanBeShown = (option: NpcInteraction) => {
  let canBeShown = true;
  if (option.playerRequirements)
    option.playerRequirements.forEach(
      (condition) => (canBeShown &&= checkCondition("player", condition))
    );
  if (!canBeShown) return false;
  if (option.npcRequirements)
    option.npcRequirements.forEach(
      (condition) => (canBeShown &&= checkCondition("npc", condition))
    );
  if (!canBeShown) return false;
  if (option.settingsRequirements)
    option.settingsRequirements.forEach(
      (condition) => (canBeShown &&= checkCondition("settings", condition))
    );
  if (!canBeShown) return false;
  if (option.inventoryRequirements)
    option.inventoryRequirements.forEach(
      (itemName) => (canBeShown &&= window.Player.has(itemName))
    );
  if (!canBeShown) return false;
  if (option.locationRequirements)
    canBeShown = option.locationRequirements.includes(Variables().scenery);
  if (!canBeShown) return false;
  if (option.locationExclusions)
    option.locationExclusions.forEach(
      (location) => (canBeShown &&= (location != Variables().scenery))
    );
  if (!canBeShown) return false;
  //TODO: also check room inventory for locationRequirements
  //ComplexRequirements should be used on a parent choice, to disable it when the conditions for none of its children are met.
  if (option.complexRequirements) {
    let complexCanBeShown = false;
    option.complexRequirements.forEach((combination) => {
      canBeShown = true;
      if (combination.playerRequirements)
        combination.playerRequirements.forEach(
          (condition) => (canBeShown &&= checkCondition("player", condition))
        );
      if (combination.npcRequirements)
        combination.npcRequirements.forEach(
          (condition) => (canBeShown &&= checkCondition("npc", condition))
        );
      if (combination.settingsRequirements)
        combination.settingsRequirements.forEach(
          (condition) => (canBeShown &&= checkCondition("settings", condition))
        );
      if (combination.inventoryRequirements)
        combination.inventoryRequirements.forEach(
          (itemName) => (canBeShown &&= window.Player.has(itemName))
        );
      if (combination.locationRequirements)
        canBeShown = option.locationRequirements.includes(Variables().scenery);
      if (option.locationExclusions)
        option.locationExclusions.forEach(
          (location) => (canBeShown &&= (location != Variables().scenery))
        );
      if (canBeShown) complexCanBeShown = true;
    });
    canBeShown = complexCanBeShown;
  }
  if (!canBeShown) return false;
  if (option.canBeShown) canBeShown = option.canBeShown();
  return canBeShown;
};
//Outputs the current interaction indicated by the route $npcInteractionRoute and directed to the Npc in $npc
//Not recommended to use it directly unless you know exactly what are you doing, use openNpcInteraction macro instead.
Macro.add("npcInteraction", {
  handler: function () {
    let vars = variables() as any;
    vars.npc = window.Person.get(vars.npc.uid);
    const npc: Npc = vars.npc;
    const steps: string[] = vars.npcInteractionRoute.split(".");
    const collection = window.Interactions[steps[0]];
    let options = callOrGetItself(collection.options);
    let interaction: NpcInteraction;
    for (let stepIndex = 1; stepIndex < steps.length; stepIndex++) {
      interaction = options[steps[stepIndex]];
      if (interaction == undefined) {
        console.error(vars.npcInteractionRoute);
        console.error(options);
        console.error(steps[stepIndex]);
      }
      options = callOrGetItself(interaction.next);
    }
    $(document.createElement("span"))
      .wiki((interaction ? interaction.contents : collection.contents) + "\n")
      .appendTo(this.output);
    if (interaction && interaction.altOptions)
      options = interaction.altOptions(npc, options);
    let result = "";
    let npcHungerIncrease = 0;
    if (interaction && interaction.minutesCost) {
      const minutes = interaction.altMinutes
        ? interaction.altMinutes(interaction.minutesCost)
        : interaction.minutesCost;
      window.Now.addMinutes(minutes);
      if (
        (collection.timeIncreaseNpcHunger ||
          interaction.timeIncreaseNpcHunger) &&
        interaction.showNpcStats &&
        interaction.timeIncreaseNpcHunger !== false
      )
        npcHungerIncrease = Math.max(1, Math.round((minutes / 8) * 0.46));
    }
    let showNpcStats = interaction ? interaction.showNpcStats : undefined;
    const extraNpcStats = Temporary().npcStatModifiers;
    if ((interaction && interaction.npcStats) || extraNpcStats) {
      const npcStats = [];
      if (interaction && interaction.npcStats) {
        const stats = callOrGetItself(interaction.npcStats, npc);
        if (stats) npcStats.push(...stats);
      }
      if (extraNpcStats) {
        const stats = callOrGetItself(extraNpcStats, npc);
        if (stats) npcStats.push(...stats);
      }
      if (npcStats.length) {
        if (showNpcStats === undefined) showNpcStats = true;
        if (
          npcHungerIncrease &&
          !npcStats.firstOrDefault((s: string) => s.startsWith("hunger"))
        )
          npcStats.push("hunger+" + npcHungerIncrease);
        result += "@@color:yellow;";
        let first = true;
        npcStats.forEach((change) => {
          switch (change) {
            case "+aroused":
              window.Now.addTimedEvent(
                0.5, //Schedule this NPC to lose its arousal in half an hour from now.
                `var p=window.Person.get(${npc.uid});if(p)p.aroused = false`,
                npc.uid + "arousalEnd"
              );
              break;
            case "-aroused":
              window.Now.removeTimedEvent(npc.uid + "arousalEnd");
              break;
          }
          if (first) first = false;
          else result += ", ";
          let varName: string;
          let varPath: string;
          let value: number | string;
          switch (change[0]) {
            case "+":
            case "-":
              varName = change.slice(1);
              varPath = "variables().npc." + varName;
              result += change[0] + varName.beautifyStat();
              value = change[0] != "-" ? "true" : "false";
              break;
            default:
              let match = /(\w+)(%?)([+-])(\d+)(%?)/.exec(change);
              varName = match[1];
              varPath = "variables().npc." + varName;
              value = eval(varPath) as number;
              if (match[2] == "%") {
                try {
                  //Fairmath
                  let max = parseInt(match[4]);
                  let fraction = (max + 1 - value) * (max / 100);
                  let term =
                    fraction * 100 == max
                      ? "0"
                      : Math.max(0, Math.round(fraction)).toString();
                  result += varName.beautifyStat() + match[3] + term;
                  value = eval(value.toString() + match[3] + term) as number;
                } catch (err) {
                  console.error(err);
                  console.info(varPath);
                }
              } else {
                result += change.beautifyStat();
                value = (
                  match[5] != "%"
                    ? eval(value + match[3] + match[4])
                    : eval(
                        `Math.max(1, value)${match[3]}${
                          (value * parseFloat(match[4])) / 100
                        }`
                      )
                ) as number;
              }
              value = Math.ceil(value).clamp(0, 100);
              break;
          }
          eval(`${varPath} = ${value}`);
        });
        result += "@@\n";
      }
    }
    if (interaction && showNpcStats) result += "<<npcStats>>\n";
    let baseRoute =
      interaction && interaction.baseRoute
        ? interaction.baseRoute(npc)
        : vars.npcInteractionRoute;
    if (vars.player.energy <= 0)
      result += `@@color:red;You REALLY need to sleep@@<br>
      <<keyAction Sleep 😴>><<sleep>><</keyAction>>`;
    else {
      for (const name in options) {
        let option = options[name];
        let canBeShown = checkCanBeShown(option);
        if (!canBeShown) {
          if (option.showDisabled) {
            let disabledText = option.showDisabled;
            if (disabledText.includes("=>")) {
              let components = disabledText.split("=>");
              if (!checkCondition("npc", components[0])) continue;
              disabledText = components[1];
            } else if (option.canBeShown && !option.canBeShown()) continue;
            let optionText = option.optionText
              .replace(/'/g, "\\'")
              .replace(/"/g, """);
            let emoji = "";
            if (/^\p{Extended_Pictographic}/u.test(optionText)) {
              emoji = optionText.split(" ")[0];
              optionText = optionText.slice(emoji.length + 1);
            }
            result += `\n<<keyDisabled '${optionText}' ${emoji}>> [${disabledText}]`;
          }
          continue;
        }
        if (
          option.showIfEmpty === false ||
          (collection.hideEmptyOptions && option.next && !option.showIfEmpty)
        ) {
          let next = callOrGetItself(option.next);
          if (option.altOptions) next = option.altOptions(npc, next);
          let empty = true;
          for (const nextName in next)
            if (nextName != "back" && checkCanBeShown(next[nextName])) {
              empty = false;
              break;
            }
          if (empty) continue;
        }
        let optionText = option.optionText
          .replace(/'/g, "\\'")
          .replace(/"/g, """);
        let emoji = "";
        if (/^\p{Extended_Pictographic}/u.test(optionText)) {
          emoji = optionText.split(" ")[0];
          optionText = optionText.slice(emoji.length + 1);
        }
        let action = option.action
          ? option.contents
          : `<<openNpcInteraction ${baseRoute}.${name}>>`;
        result += `\n<<keyAction '${optionText}' ${emoji}>>${action}<</keyAction>>`;
        if (option.minutesCost) result += `: ${option.minutesCost}min`;
      }
      let stopOptionText: string;
      if (interaction)
        stopOptionText =
          interaction.stopOption === false ? null : interaction.stopOption;
      if (!stopOptionText && stopOptionText !== null) {
        stopOptionText =
          collection.defaultStopOption === false
            ? null
            : collection.defaultStopOption;
      }
      if (stopOptionText !== null) {
        stopOptionText = !stopOptionText
          ? "🔙 Return"
          : stopOptionText.replace(/'/g, "\\'").replace(/"/g, """);
        let emoji = "";
        if (/^\p{Extended_Pictographic}/u.test(stopOptionText)) {
          emoji = stopOptionText.split(" ")[0];
          stopOptionText = stopOptionText.slice(emoji.length + 1);
        }
        if (collection.beforeStop)
          result += `\n<<keyAction '${stopOptionText}' ${emoji}>>${collection.beforeStop}<<goto $returnPassage>><</keyAction>>`;
        else
          result += `\n<<keyOption [[${stopOptionText}|$returnPassage]] ${emoji}>>`;
      }
    }
    $(document.createElement("span")).wiki(result).appendTo(this.output);
  },
});
//Outputs a text based on the current person uniqueness in $npc or passed as the first parameter
//Uses the tables in the UniquenessTables class.
Macro.add("personUniqueness", {
  handler: function () {
    let person: Person = this.args[1] || Variables().npc;
    const table: any[][] = UniquenessTables[this.args[0]];
    let defaultCase: UniquenessCase;
    const checkCondition = (condition: string) => {
      if (!condition) return false;
      var match = condition.match(/^([a-z]+)([^\d]*)(\d+)$/);
      return eval(person[match[1]] + (match[2] || ">=") + match[3]);
    };
    const processAgeRow = (cases: Array<UniquenessCase>) => {
      defaultCase = cases.firstOrDefault(
        (uniquenessCase: UniquenessCase) => !uniquenessCase.condition
      );
      let conditionIndex = table[0].length;
      for (let caseIndex = cases.length - 1; caseIndex >= 0; caseIndex--) {
        const uniquenessCase = cases[caseIndex];
        if (!uniquenessCase.condition) {
          if (uniquenessCase == defaultCase) continue;
          if (checkCondition(table[0][--conditionIndex])) {
            setOutput(uniquenessCase);
            return;
          }
        } else if (checkCondition(uniquenessCase.condition)) {
          setOutput(uniquenessCase);
          return;
        }
      }
      setOutput(defaultCase);
    };
    const setOutput = (
      uniquenessCase: UniquenessCase,
      specificField?: string
    ) => {
      let output: string;
      if (!specificField)
        for (let fieldName in uniquenessCase) {
          switch (fieldName) {
            case "condition":
            case "default":
            case "stats":
              continue;
            default:
              let okOutput = true;
              if (fieldName.includes("And"))
                fieldName
                  .split("And")
                  .forEach(
                    (c) => (okOutput &&= person.uniqueness[c.toLowerCase()])
                  );
              else okOutput = person.uniqueness[fieldName];
              if (okOutput) output = uniquenessCase[fieldName];
              else continue;
              break;
          }
          break;
        }
      else output = uniquenessCase[specificField];
      if (!output) output = uniquenessCase.default;
      if (output == "=default") {
        setOutput(defaultCase);
        return;
      } else if (output.startsWith("=age")) {
        processAgeRow(
          table
            .firstOrDefault<any[]>((row: any[]) => row[0] == output.slice(4))
            .slice(1)
        );
        return;
      } else if (output[0] == "=") {
        let fieldName = output.substring(1);
        output = uniquenessCase[fieldName];
        if (!output) {
          setOutput(defaultCase, fieldName);
          return;
        }
      }
      if (uniquenessCase.stats)
        Temporary().npcStatModifiers = uniquenessCase.stats;
      output = output.replace(/^say:(.+)/i, `''${person.name}'': "$1"`);
      $(document.createElement("span")).wiki(output).appendTo(this.output);
    };
    for (let ageIndex = table.length - 1; ageIndex > 0; ageIndex--) {
      if (table[ageIndex][0] > person.age) continue;
      processAgeRow(table[ageIndex].slice(1));
      return;
    }
  },
});
//To use in an interaction to indicate NPC ejaculating
Macro.add("npcCum", {
  handler: function () {
    const $npc: Npc = Variables().npc;
    const lustDecCum = Variables().settings.lustDecCum;
    if (!lustDecCum && $npc.hasPussy) return;
    if (!Temporary().npcStatModifiers) Temporary().npcStatModifiers = [];
    const _npcStatModifiers = Temporary().npcStatModifiers;
    if (lustDecCum) _npcStatModifiers.push("lust-" + lustDecCum); //decrease as much lust as said in the settings
    if (!$npc.hasPussy) _npcStatModifiers.push("-aroused");
  },
});
//To use in an interaction to indicate NPC is stimulated.
Macro.add("npcStimulated", {
  handler: () => {
    const $npc: Npc = Variables().npc;
    if (!$npc.aroused) {
      if (!Temporary().npcStatModifiers) Temporary().npcStatModifiers = [];
      Temporary().npcStatModifiers.push("+aroused");
    } else window.Now.assertTimedEvent($npc.uid + "arousalEnd", 0.5);
  },
});

repo_name: Carrabina/the


code:
interface BodyOverlay {
  sex: Gender;
  imageData: string;
}
type BodyState = "normal" | "pregnant";
interface Position {
  x: number;
  y: number;
}
interface BodyGraphic {
  sex?: Gender;
  overlay?: BodyOverlay;
  state?: BodyState;
  data: string;
  position: Position;
}
interface FaceExpressions {
  neutral: string;
  scared: string;
  mildSmile: string;
}
interface HairGraphic {
  back: string;
  front: string;
}
type ClothingType =
  | "head"
  | "neck"
  | "top"
  | "hand"
  | "bottom"
  | "socks"
  | "shoes"
  | "underwear";
interface ClothesGraphics {
  types: ClothingType[];
  set: string;
  data: string;
  position: Position;
  name: string;
  colorized: boolean;
}
interface GrowthStageGraphics {
  body: BodyGraphic[];
  hair: Record<string, Record<string, HairGraphic> | HairGraphic>; //Style and optional style variant (i.e. length)
  clothes: Record<string, ClothesGraphics>;
}
interface GraphicPose {
  stages: Record<PersonGrowthStage, GrowthStageGraphics>;
  faceExpressions: FaceExpressions;
  blush: string;
  expressionPosition: Position;
  blushPosition: Position;
}


code:
class TimedEvent {
  //The minimum number of hours to pass in order to trigger this event.
  timeoutHours: number;
  //The action to execute when this event is triggered, stored in a string so it can be stored in SugarCube history and save.
  action: string;
  //Reference to identify this event and modify or delete it.
  ref: any;
  //If a reference is not provided the contents of the action will be used as reference instead.
  constructor(timeoutHours: number, javascript: string, ref?: any);
  constructor(timeoutHours: number, action: () => void, ref?: any);
  constructor(
    timeoutHours: number,
    actionOrJavascript: (() => void) | string,
    ref?: any
  ) {
    this.timeoutHours = timeoutHours;
    this.action = actionOrJavascript.toString();
    if (ref) this.ref = ref;
    else this.ref = this.action;
  }
}
class Now {
  //The game has a full date/time and this is the date/time where the game starts.
  //It's also the Monday of the week when this was implemented.
  //This property will always have this value, the current game date/time is stored in $now.date
  date: Date = new Date(2022, 3, 4, 7);
  getCurrentDate(): Date {
    return Variables().now.date;
  }
  //Converts a time string to a Javascript Date object assuming the time is within the current day or the one passed as a reference (ref optional parameter)
  //The string should be something like "7:30 PM"
  dateFromTimeString(timeString: string, ref?: Date): Date {
    if (!ref) ref = this.getCurrentDate();
    return new Date(
      ref.toLocaleString("en-us", {
        year: "numeric",
        month: "numeric",
        day: "numeric",
      }) +
        " " +
        timeString
    );
  }
  //Checks if the current date/time is equal or later than the provided time string that will be considered on the same day as currentDate.
  //If currentDate is not provided Now.getCurrentDate() will be used instead.
  //Example: if currentDate is on 11:59 PM Now.isEqualOrLaterThan('7:30 PM') will return true, but false if currentDate is on 12:00 AM (first hour of day)
  isEqualOrLaterThan(timeString: string, currentDate?: Date): boolean {
    if (!currentDate) currentDate = this.getCurrentDate();
    return (
      currentDate.getTime() >=
      this.dateFromTimeString(timeString, currentDate).getTime()
    );
  }
  //Same as isEqualOrLaterThan but it will check if it's earlier instead.
  //12:00 AM will always return true since it's the start of the day.
  isEqualOrEarlierThan(timeString: string, currentDate?: Date): boolean {
    if (!currentDate) currentDate = this.getCurrentDate();
    return (
      currentDate.getTime() <=
      this.dateFromTimeString(timeString, currentDate).getTime()
    );
  }
  //Checks if currentDate time it's between the two provided time strings.
  //It works even if from is in the evening and to is in the morning to check a period in the night time.
  //If currentDate is not provided Now.getCurrentDate() will be used instead.
  isBetween(
    fromTimeString: string,
    toTimeString: string,
    currentDate?: Date
  ): boolean {
    if (!currentDate) currentDate = this.getCurrentDate();
    var currentTimeStamp = currentDate.getTime();
    var fromTimeStamp = this.dateFromTimeString(
      fromTimeString,
      currentDate
    ).getTime();
    var toTimeStamp = this.dateFromTimeString(
      toTimeString,
      currentDate
    ).getTime();
    if (fromTimeStamp < toTimeStamp)
      return (
        currentTimeStamp >= fromTimeStamp && currentTimeStamp <= toTimeStamp
      );
    if (currentDate.getHours() < 12) return currentTimeStamp <= toTimeStamp;
    return currentTimeStamp >= fromTimeStamp;
  }
  //Checks if now it's exactly the time specified
  is(timeString: string): boolean {
    var currentDate = this.getCurrentDate();
    return (
      currentDate.getTime() ==
      this.dateFromTimeString(timeString, currentDate).getTime()
    );
  }
  //This action is used like an event. Every time a day or more passes this action should be called.
  daysPassed(amount: number): void {
    if (amount < 1) return;
    let variables = Variables();
    let player = variables.player as Player;
    player.workedToday = false;
    let slaves = variables.slaves as Person[];
    for (let index = 0; index < amount; index++) {
      slaves.forEach((slave) => {
        slave.aroused = false;
        slave.lubricatedAss = false;
        slave.lubricatedPussy = false;
        slave.fear = Math.max(0, slave.fear - 10);
        slave.hunger = Math.min(100, slave.hunger + 10);
        slave.freedomWish = Math.max(0, slave.freedomWish - 5);
        slave.lust = Math.max(0, slave.lust - 1);
        if (slave.hunger >= 90)
          slave.love = Math.max(0, slave.love - (slave.hunger - 80));
        if (slave.punishments.includes("naked")) {
          if (slave.obedience < 60)
            slave.obedience += Math.round((61 - slave.obedience) * 0.25);
          else slave.punishments.delete("naked");
        } else slave.obedience = Math.max(0, slave.obedience - 1);
        window.Person.removeAchievement("howAreYou", slave);
      });
      player.lust = Math.min(100, player.lust + 10);
    }
  }
  //Same as above but with hours, also to be used with minutes by giving hour fractions.
  //This event it's independent of daysPassed if hours pass and day changes both events must be called.
  hoursPassed(amount: number): void {
    window.Player.manageEnergy(amount);
    const timedEvents = Variables().timedEvents as TimedEvent[];
    if (timedEvents) {
      for (
        let index = timedEvents.length - 1;
        index < timedEvents.length;
        index++
      ) {
        const element = timedEvents[index];
        element.timeoutHours -= amount;
        if (element.timeoutHours <= 0) {
          eval(element.action);
          timedEvents.splice(index);
        }
      }
      if (!timedEvents.length) Variables().timedEvents = undefined;
    }
  }
  //Adds a timed event that triggers after the specified number of hours (or fraction of hours) has passed. See TimedEvent class for details.
  //Providing a reference object or value to the event it's optional. But necessary if you need to modify or check the event afterwards.
  //If a reference is not provided the contents of the action will be used as reference instead.
  //If a timed event with the same reference exist, that event timeout will be extended instead of creating a new one.
  addTimedEvent(timeoutHours: number, action: () => void, ref?: any): void;
  addTimedEvent(timeoutHours: number, javascript: string, ref?: any): void;
  addTimedEvent(
    timeoutHours: number,
    actionOrJavascript: (() => void) | string,
    ref?: any
  ): void {
    if (!Variables().timedEvents) Variables().timedEvents = [];
    const javascript = actionOrJavascript.toString();
    if (!ref) ref = javascript;
    const existing = this.getTimedEventByRef(ref);
    if (existing) {
      existing.timeoutHours = timeoutHours;
      return;
    }
    Variables().timedEvents.push(new TimedEvent(timeoutHours, javascript, ref));
  }
  //Gets a timed event by its reference.
  getTimedEventByRef(ref: any): TimedEvent {
    const timedEvents = Variables().timedEvents;
    if (!timedEvents) return null;
    return timedEvents.firstOrDefault((e: TimedEvent) => e.ref === ref);
  }
  //Extend a specific timed event (with the provided reference) by the specified hours (adds hours to the timeout)
  extendTimedEvent(ref: any, addHours: number): void {
    const event = this.getTimedEventByRef(ref);
    if (!event) return;
    event.timeoutHours += addHours;
  }
  //Erases the first timed event with the specified reference
  removeTimedEvent(ref: any): void {
    const timedEvents: TimedEvent[] = Variables().timedEvents;
    if (!timedEvents) return;
    for (let index = 0; index < timedEvents.length; index++)
      if (timedEvents[index].ref === ref) {
        timedEvents.splice(index);
        break;
      }
    if (!timedEvents.length) Variables().timedEvents = undefined;
  }
  //Makes sure that a specific timed event (with the provided reference) has at least the hours specified.
  assertTimedEvent(ref: any, hours: number): void {
    const event = this.getTimedEventByRef(ref);
    if (!event) return;
    event.timeoutHours = Math.max(event.timeoutHours, hours);
  }
  //Should be called when current game time changes.
  timeChanged(currentDate: Date) {
    Npc.updateLocations(currentDate);
    let variables = Variables();
    let cook = variables.settings.cook;
    if (!cook || !cook.npc) return;
    let today =
      currentDate.getFullYear() * 1e4 +
      currentDate.getMonth() * 1e2 +
      currentDate.getDate();
    for (let index = 0; index < cook.feedTimes.length; index++)
      if (!cook.lastFeedings[index] || cook.lastFeedings[index] < today)
        if (this.isEqualOrLaterThan(cook.feedTimes[index], currentDate)) {
          for (let slave of variables.slaves) {
            let config =
              cook.exceptions.firstOrDefault((e: any) => e.npc == slave.uid) ??
              cook;
            if (!config.feedEnabled) continue;
            if (slave.hunger >= config.feedAtHunger)
              slave.hunger -= Math.max(5, slave.hunger - config.feedAtHunger);
          }
          cook.lastFeedings[index] = today;
        }
  }
  //Makes the specified number of hours pass in the game.
  addHours(amount: number): void {
    var currentDate = this.getCurrentDate();
    var originalDay = currentDate.getDay();
    currentDate.setHours(currentDate.getHours() + amount);
    this.daysPassed(currentDate.getDay() - originalDay);
    this.hoursPassed(amount);
    this.timeChanged(currentDate);
  }
  //Skip time until the specified time of the day is reached (Even if it's on the next day).
  skipTo(timeString: string): void {
    let currentDate = this.getCurrentDate();
    let target = this.dateFromTimeString(timeString, currentDate);
    if (target.getTime() <= currentDate.getTime()) {
      target.setDate(target.getDate() + 1);
      this.daysPassed(1);
    }
    this.hoursPassed(Math.abs(target.getTime() - currentDate.getTime()) / 36e5);
    currentDate.setTime(target.getTime());
    this.timeChanged(currentDate);
  }
  //Makes the specified number of minutes pass in the game.
  addMinutes(amount: number): void {
    var currentDate = this.getCurrentDate();
    var originalDay = currentDate.getDay();
    currentDate.setMinutes(currentDate.getMinutes() + amount);
    this.daysPassed(currentDate.getDay() - originalDay);
    this.hoursPassed(amount / 60);
    this.timeChanged(currentDate);
  }
  //Returns the name of the current weekday in English
  getWeekDay(): string {
    return this.getCurrentDate().toLocaleString("en-us", { weekday: "long" });
  }
  //Returns the current time string in English format. Something like 7:30 PM
  getTime(): string {
    return this.getCurrentDate().toLocaleString("en-us", {
      hour: "numeric",
      minute: "numeric",
      hour12: true,
    });
  }
  //Checks if the game date is currently on a weekend.
  isWeekend(): boolean {
    var weekDay = this.getCurrentDate().getDay();
    return weekDay == 0 || weekDay == 6;
  }
}


code:
//Biological characteristics
type Sex = "male" | "female" | "herm";
//Social characteristics
type Gender = "boy" | "girl" | "nb";
type Genitals = "cunny" | "pussy" | "penis" | "dick";
type AllGenitals = {
  male: Genitals;
  female: Genitals;
  all: string;
};
interface Home {
  name: string;
  rent: number;
  //Spaces that this house have using the names of the main passages on that space
  spaces: string[];
}
//Returns the SugarCube variables object in Typescript format (variables saved in history).
function Variables(): any {
  return variables() as any;
}
//Returns the SugarCube temporary object in Typescript format (variables saved in memory that will vanish on the next passage).
function Temporary(): any {
  return temporary() as any;
}
class Player {
  //Zero UID identifies the player.
  uid: Uid = 0;
  name: string;
  sex: Sex;
  gender: Gender;
  genitals: AllGenitals;
  hasPussy: boolean;
  hasPenis: boolean;
  hasBoobs: boolean;
  lactating: boolean;
  //The name of the hole where the player can be fucked by default (not sure if this will be used)
  sexHole: string;
  home: Home = Homes.smallUrban;
  job: Job = Jobs.garbageCollector;
  cash: number = 100;
  energy: number = 100;
  //Unused, not sure if it will ever be used.
  lust: number;
  //Set to true when the player goes to sleep so the bed passage knows that the player just woke up and to know if energy should be reduced or restored
  sleeping: boolean;
  //Just a flag to check if the player has worked already to avoid evading responsibilities.
  workedToday: boolean = false;
  //Holds the items that the player has available to use.
  inventory: Inventory = new Inventory();
  //Gets the player inventory object from the Sugarcube variables.
  getInventory() {
    return new Inventory(Variables().player.inventory);
  }
  //Checks if the player has an item in their inventory.
  has(itemName: string, count: number = 1) {
    return this.getInventory().has(itemName, count);
  }
  removeItem(itemName: string) {
    return this.getInventory().removeByName(itemName);
  }
  manageEnergy(hoursPassed: number) {
    let player = Variables().player as Player;
    let multiplier =
      player.sleeping || Variables().settings.infiniteEnergy ? 1 : -0.46;
    let increment = Math.round((hoursPassed / 8) * 100 * multiplier);
    if (increment == 0) increment = multiplier < 0 ? -1 : 1;
    player.energy = (player.energy + increment).clamp(0, 100);
  }
  setGender(gender: Gender) {
    let player = Variables().player as Player;
    player.gender = gender;
  }
  //Use this to change the player's sex.
  setSex(sex: Sex, player?: Player) {
    if (!player) player = Variables().player as Player;
    player.sex = sex;
    if (sex == "male") {
      player.genitals = {
        male: "dick",
        female: null,
        all: "dick",
      };
      player.hasPenis = true;
      player.hasPussy = false;
      player.hasBoobs = false;
      player.sexHole = "ass";
    }
    if (sex == "female") {
      player.genitals = {
        male: null,
        female: "pussy",
        all: "pussy",
      };
      player.hasPenis = false;
      player.hasPussy = true;
      player.hasBoobs = true;
      player.sexHole = "pussy";
    }
    if (sex == "herm") {
      player.genitals = {
        male: "dick",
        female: "pussy",
        all: "dick and pussy",
      };
      player.hasPenis = true;
      player.hasPussy = true;
      player.hasBoobs = true;
      player.sexHole = "pussy";
    }
  }
  //Achievements are used to keep track of what things are already done before.
  hasAchievement(achievement: string) {
    return (Variables().achievements as string[]).includes(achievement);
  }
  //Get how the specified NPC calls the player (how is the player addressed)
  getAddressing(npc: Npc) {
    let variables = Variables();
    let $player: Player = variables.player;
    if (npc.status == "citizen")
      return $player.gender != "boy" ? "lady" : "mister";
    let $addressing = variables.settings.addressing;
    if (npc.mom == this.uid || npc.dad == this.uid)
      return $addressing && $addressing.offspring
        ? $addressing.offspring
        : $player.gender != "boy"
        ? "mommy"
        : "daddy";
    let result = $player.name;
    if (!$addressing) return result;
    if ($addressing.slave) result = $addressing.slave;
    if (npc.status == "slave") return result;
    var specific = $addressing[npc.status];
    return specific ? specific : result;
  }
  //Bind a HTML DOM element with a property. So that when the element value is changed the property changes too and the other way around.
  //Parameters:
  //id: ID of the HTML element that will be bind to the variable
  //parentVariable: The parent variable or source property that contains the target property
  //propertyName: The name of the target property in the parent variable
  //onChanged: (optional, default:null) Extra function to execute when the element value is changed. If this function returns a value, that value will be used instead of the element's value.
  //displayId: (optional) Only for range elements. It will apply the range value to the input with the specified id. So the player can see its current value.
  bindSettingDom(
    id: string,
    parentVariable: object,
    propertyName: string,
    onChanged: (element: HTMLElement) => any,
    displayId: string
  ): void {
    let $element = $("#" + id);
    switch ($element.attr("type")) {
      case "checkbox":
        $element
          .on("change", function () {
            if (onChanged) parentVariable[propertyName] = onChanged(this);
            if (!onChanged || parentVariable[propertyName] === undefined)
              parentVariable[propertyName] = (<HTMLInputElement>this).checked;
          })
          .prop("checked", parentVariable[propertyName]);
        break;
      case "range":
        $element
          .on("input", function () {
            if (onChanged) parentVariable[propertyName] = onChanged(this);
            if (!onChanged || parentVariable[propertyName] === undefined)
              parentVariable[propertyName] = parseInt(
                (<HTMLInputElement>this).value
              );
            if (displayId) $("#" + displayId).val(parentVariable[propertyName]);
          })
          .val(parentVariable[propertyName] || 0);
        break;
      case "number":
        $element
          .on("change", function () {
            if (onChanged) parentVariable[propertyName] = onChanged(this);
            if (!onChanged || parentVariable[propertyName] === undefined)
              parentVariable[propertyName] = $(this).val();
          })
          .val(parentVariable[propertyName] || 0);
        break;
    }
  }
  //Bind a pair of HTML DOM elements indicating a range with their respective properties of a variable. So that when the element value is changed the property changes too and the other way around.
  //Parameters:
  //elementIdBase: Used to get the id of the HTML elements. "From" and "To" will be appended to get the two element's IDs
  //parentVariable: The variable that contains the from and to properties to bind.
  //propertyBaseName: Used to get the name of the properties. The first letter will be uppercase'd, and "from" and "to" will be prepended to get the two property names.
  bindRangeSettingsDom(
    elementIdBase: string,
    parentVariable: object,
    propertyBaseName: string
  ): void {
    let $from = $("#" + elementIdBase + "From");
    let $to = $("#" + elementIdBase + "To");
    $from
      .on("change", function () {
        let toValue = parseInt(<string>$to.val());
        let $this = $(this);
        let value = Math.min(toValue, parseInt(<string>$this.val()));
        $this.val(value);
        parentVariable["from" + propertyBaseName.toUpperFirst()] = value;
      })
      .val(parentVariable["from" + propertyBaseName.toUpperFirst()]);
    $to
      .on("change", function () {
        let fromValue = parseInt(<string>$from.val());
        let $this = $(this);
        let value = Math.max(fromValue, parseInt(<string>$this.val()));
        $this.val(value);
        parentVariable["to" + propertyBaseName.toUpperFirst()] = value;
      })
      .val(parentVariable["to" + propertyBaseName.toUpperFirst()]);
  }
}
/* Snippet to hook an event to an object. Might be useful someday.
interface ILiteEvent<T> {
  on(handler: { (data?: T): void }): void;
  off(handler: { (data?: T): void }): void;
}
class LiteEvent<T> implements ILiteEvent<T> {
  private handlers: { (data?: T): void }[] = [];

  public on(handler: { (data?: T): void }): void {
    this.handlers.push(handler);
  }

  public off(handler: { (data?: T): void }): void {
    this.handlers = this.handlers.filter((h) => h !== handler);
  }

  public trigger(data?: T) {
    this.handlers.slice(0).forEach((h) => h(data));
  }
}*/
interface String {
  /**
   * Split camelcase words and upper the first letter.
   */
  beautifyStat(): string;
}
//Split camelcase words and upper the first letter.
String.prototype.beautifyStat = function () {
  return this.toUpperFirst().replace(/(\B[A-Z])/g, " $1");
};
interface Array<T> {
  getSentence(): string;
}
//Constructs a sentence from an array of strings ["1","2","3"] becomes "1, 2 and 3"
Array.prototype.getSentence = function () {
  if (!this.length) return "";
  if (this.length == 1) return this[0];
  let clone = this.slice();
  let last = clone.pop();
  return clone.join(", ") + " and " + last;
};
//Useful class to make a less random generator than the one in Math or in Sugarcube.
//Two identical results appearing together is way les likely with this class if a correlative seed is provided.
class PseudoRandom {
  //Generates a seed from other objects. Player/npc stats, traits and name can be used, recommended to include something incremental like turns() or the current time.
  static getSeed(...components: any[]): number {
    let seed = 0;
    for (const index in components) {
      const component = components[index];
      switch (typeof component) {
        case "boolean":
        case "number":
          seed += component as number;
          break;
        case "string":
          for (let charIndex = 0; charIndex < component.length; charIndex++)
            seed = (seed << 5) - seed + component.charCodeAt(charIndex);
          break;
      }
    }
    return Math.abs(seed);
  }
  //Get a pseudorandom integer using the provided seed
  static getInt(seed: number): number {
    //27 is a coprime of 100 and 10-1 is divisible by 27's factors (3)
    return (27 * seed + 10) % 100;
  }
  //Get a pseudorandom floating comma number using the provided seed
  static getFloat(seed: number): number {
    return this.getInt(seed) / 100;
  }
  //Get a random integer number from a range of integer numbers using the provided seed
  static getFromRange(seed: number, start: number, end: number): number {
    return start + Math.floor(this.getFloat(seed) * (end - start));
  }
  //Get a random item from the provided array using the provided seed.
  static either<T>(seed: number, options: Array<T>): T {
    return options[this.getFromRange(seed, 0, options.length)];
  }
}
type Uid = number;
//Gets a unique identification number to identify objects, items, individuals or even places that can be generated.
//Since all other properties of an object has the possibility to be changed (even name) this can be used to save in an npc the Uid of their mother for instance.
//The zero Uid is reserved to identify the player.
function getUid(variables?: any): Uid {
  if (!variables) variables = Variables();
  variables.lastUid = variables.lastUid ? variables.lastUid + 1 : 1;
  return variables.lastUid;
}


code:
interface UniquenessCase {
  //
  stats: any;
  //default option, when it doesn't match any other characteristic
  default: string;
  //When specified, this case will follow this condition instead of the global ones.
  condition?: string;
  //The other properties are tied to the npc traits. They can be combined with "And" in camelcase. Example: curiousAndNaughty
  //The traits will be checked in the same order that they are coded.
  //That is, if shy is specified first and then energetic. Npc shyness will be checked first and then the npc energetic trait (the first one that is true)
  curious?: string;
  naughty?: string;
  energetic?: string;
  shy?: string;
  diligent?: string;
}
//This static class stores all data needed to get the right Sugarcube markup according to the NPC stats and traits.
//This is to make it easier to write and maintain reaction mechanics that need multiple checks.
//Otherwise that would need lots of nested <<if>> macros that would make it easy to fuck up, specially when changing things.
//To call one of the tables (array properties in this class) use the <<personUniqueness>> macro.
//Example: <<personUniqueness hug>> (See npcInteraction.ts)
class UniquenessTables {
  static howAreYou = [
    ["fear<20", "love60", "lust80"], //global conditions for each line without specified condition
    [
      //Start of the fist age array
      0, //The fist element in the array is the minimum age to take this array into account
      {
        //The next elements are uniqueness cases in objects (See UniquenessCase interface)
        //first object without specified condition is default output
        default: "$npc.name looks at you without saying anything.", //default option, when it doesn't match any other characteristic
        shy: "$npc.name does not say anything and tries to avoid eye contact.", //output on shy characteristic
      },
      {
        //fear<20 condition
        default: "$npc.name looks at you with a mild smile<<emoji 🙂>>",
        shy: "=default", //same as shy in default condition (not default characteristic)
      },
      {
        //love60
        default: "$npc.name smiles at you and blushes a little<<emoji 😊>>",
        shy: "$npc.name is blushes a lot while showing a mild smile<<emoji 🙂>>",
        energetic: `say:<<npcAddressPlayer>>!!
        $npc.GenPronoun <<thirdPerson "says" "say">> with a big smile as $npc.pronoun jumps on you.`, //say: is a shortcut to <<npcSay>> widget using the rest of the line as input
      },
      {
        //lust80 (adding condition:"lust80" field would also do the same)
        default:
          "$npc.name doesn't answer and stretches out $npc.possessive hands towards your $player.genitals.all while making toddler sounds.",
        shy: `say:...
        ($npc.GenPronoun <<thirdPerson "blushes" "blush">> while $npc.possessive eyes keep looking between your legs.)`,
        energetic: `say:<<npcAddressPlayer>>!!
        $npc.GenPronoun <<thirdPerson "says" "say">> smiling while rubbing $npc.possessive $npc.genitals.all.`,
      },
    ],
    [
      5, //From 5 yo onwards
      {
        default: "=age0", //use the same as age0 for all personalities
      },
      {
        //fear<20
        default: "say:Fine...",
        shy: "=age0",
        energetic:
          "say:<<if $npc.freedomWish gte 40>>I wanna go home!<<emoji 😢>><<else>>I'm good!!<</if>>",
      },
      {
        //love60
        default: "say:I'm good <<npcAddressPlayer>><<emoji 😚>>",
        shy: `$npc.name blushes and smiles at you. $npc.GenPronoun <<thirdPerson "looks" "look">> happy<<emoji 🥰>>`,
        energetic: `say:I'm so happy with you <<npcAddressPlayer>>!<<emoji 😊>>
        $npc.GenPronoun <<thirdPerson "jumps" "jump">> at you and <<thirdPerson "gives" "give">> you a loving hug<<emoji 💗>>`,
      },
      {
        //lust80
        default: `say:I'm fine <<npcAddressPlayer>><<emoji 🤤>>
        ($npc.GenPronoun<<thirdPerson "'s" "'re">> shamelessly masturbating in front of you)`,
        shy: `say:I..um...want to <<if $npc.diligent || $npc.age gte 11>>have...sex...<<else>>do...that...<</if>><<emoji 😳>>
        $npc.GenPronoun <<thirdPerson "says" "say">> with a hand over $npc.possessive $npc.genitals.all. It seems that $npc.genPronoun can't bear the excitement and slowly <<thirdPerson "rubs" "rub">> it a little bit.`,
        energetic: `say:<<npcAddressPlayer>>!! Let's do naughty things!!
        $npc.GenPronoun <<thirdPerson "approaches" "approach">> you and gently <<thirdPerson "touches" "touch">> your $player.genitals.all<<emoji 😋>>`,
      },
    ],
    [
      11,
      {
        default: "say:Ok, I guess...",
        shy: "=age0",
      },
      {
        //fear<20
        default: "say:I'm fine <<npcAddressPlayer>><<emoji 🙂>>",
        shy: `say:...
        (blushes and looks down)`,
        energetic:
          "say:<<if $npc.freedomWish gte 40>>I miss my home<<emoji 😑>><<else>>I'm good!!<</if>>",
      },
      {
        default: "=age5",
      },
      {
        default: "say:I'm horny...Can we have sex? <<emoji 😛>>",
        shy: "=age5",
        energetic: `$npc.name comes closer to you and says "I'm ready anytime<<emoji 😛>>".
        $npc.GenPronoun <<thirdPerson "touches" "touch">> you gently while drooling a little bit<<emoji 🤤>>`,
      },
    ],
  ];
  static hug = [
    ["fear40", "love40", "love60", "love80", "fear60", "fear80"],
    [
      0,
      {
        default: `You grab $npc.name on your arms and give $npc.pronoun a tight loving hug<<emoji 🥰>>
          The baby feels pretty warm on your body.`,
        stats: ["love+5"],
      },
    ],
    [
      1,
      {
        //default
        default: `You open your arms to embrace $npc.name in a hug, $npc.GenPronoun <<thirdPerson "doesn't" "don't">> seem to care either way, simply allowing you to hug $npc.pronoun. $npc.GenPronoun <<thirdPerson "is" "are">> almost limp in your embrace, offering little reaction.`,
        stats: ["love+1"],
      },
      {
        //fear40
        default:
          `You open your arms to embrace $npc.name in a hug, $npc.genPronoun <<thirdPerson "takes" "take">> a fearful step back but <<thirdPerson "doesn't" "don't">> attempt to move away from you. You embrace $npc.pronoun in a hug. You can tell $npc.genPronoun <<thirdPerson "is" "are">> a bit uneasy.`,
        stats: ["fear-5"],
      },
      {
        //love40
        default: `You open your arms to embrace $npc.name in a hug, $npc.genPronoun <<thirdPerson "doesn't" "don't">> seem to mind. You can feel $npc.possessive warmth, and you even feel $npc.possessive arms gently returning the hug.`,
        stats: ["love+5", "fear-5"],
      },
      {
        //love60
        default: `You open your arms to embrace $npc.name in a hug, $npc.genPronoun <<thirdPerson "smiles" "smile">> at you and <<thirdPerson "throws" "throw">> $npc.possessive arms around you and <<thirdPerson "squeezes" "squeeze">> you tightly. You gently rock back and forth holding $npc.pronoun contentedly.`,
        naughty: `You open your arms to embrace $npc.name in a hug, $npc.genPronoun <<thirdPerson "smiles" "smile">> at you and <<thirdPerson "throws" "throw">> $npc.possessive arms around you and <<thirdPerson "squeezes" "squeeze">> you tightly. You feel $npc.possessive hand slide down your chest, and grope you before quickly returning to the hug. $npc.name's naughtiness makes you <<- $player.hasPenis?'hard':'wet'>>. 'Such a naughty little thing' you think as you smirk at $npc.pronoun.`,
        stats: ["love+20%", "fear-10", "freedomWish-10"],
      },
      {
        //love80
        default: `You open your arms to embrace $npc.name in a hug, $npc.genPronoun <<thirdPerson "doesn't" "don't">> give you time to fully open your arms before $npc.genPronoun <<thirdPerson "has" "have">> squirmed $npc.possessive way into your embrace, squeezing you as hard as $npc.genPronoun can. $npc.GenPronoun <<thirdPerson "hums" "hum">> happily as $npc.genPronoun <<thirdPerson "squeezes" "squeeze">> you.

        "I love you, <<npcAddressPlayer>>"! $npc.GenPronoun <<thirdPerson "says" "say">> as $npc.genPronoun <<thirdPerson "nuzzles" "nuzzle">> your chest.`,
        naughty: `You open your arms to embrace $npc.name in a hug, $npc.genPronoun <<thirdPerson "doesn't" "don't">> give you time to fully open your arms before $npc.genPronoun <<thirdPerson "has" "have">> squirmed $npc.possessive way into your embrace, squeezing you as hard as $npc.genPronoun can. $npc.GenPronoun <<thirdPerson "hums" "hum">> happily as $npc.genPronoun <<thirdPerson "squeezes" "squeeze">> you. $npc.GenPronoun <<thirdPerson "takes" "take">> hold of your arm and gently <<thirdPerson "pulls" "pull">> it down, guiding your hand down between $npc.possessive legs, the look on $npc.possessive face is lewd and excited. $npc.GenPronoun <<thirdPerson "is" "are">> oh so naughty.`,
        stats: ["love+20%", "fear-10", "freedomWish-20"],
      },
      {
        //fear60
        default:
          `You open your arms to embrace $npc.name in a hug, but $npc.genPronoun <<thirdPerson "pushes" "push">> against you trying to stop the embrace. You can feel $npc.pronoun trembling with fear within your arms.`,
        stats: ["fear-1"],
      },
      {
        //fear80
        default:
          `You open your arms to embrace $npc.name in a hug, but $npc.GenPronoun <<thirdPerson "is" "are">> so terrified of you $npc.genPronoun immediately <<thirdPerson "starts" "start">> crying and backing away, looking for an escape. You enclose $npc.pronoun in your embrace anyway, trying to make $npc.pronoun feel loved. $npc.GenPronoun <<thirdPerson "squirms" "squirm">> and <<thirdPerson "yells" "yell">> in a desperate attempt to escape your hold, pushing and pressing every which way trying to escape; tears run down $npc.possessive face as $npc.genPronoun <<thirdPerson "cries" "cry">>.`,
        stats: ["freedomWish+10"],
      },
    ],
  ];
  static likedPreviousNaughty = [
    [],
    [
      2,
      {
        //If none of the below personalities matches the person this will appear:
        default: "$npc.name shrugs and doesn't say anything.",
        //If the person is naughty and shy this will appear:
        naughtyAndShy: "$npc.name blushes and nods.",
        naughty: "$npc.name smiles and nods.<<emoji 🙂>>",
      },
    ],
    [
      5, //From 5 years old onwards do this:
      {
        default: "Say:I don't know...<<emoji 🙁>>",
        //If the person is naughty and shy use the same as in 2 year old.
        naughtyAndShy: "=age2",
        naughty: "Say:Yeah! I like naughty stuff!!<<emoji 😃>>",
      },
    ],
    [
      8, //From 8 years old onwards do this:
      {
        default: "=age5",
        diligent: "Say:I just did what I was told to...<<emoji 🥺>>",
      },
    ],
  ];
  static sleepPlayerNaked = [
    [],
    [
      2,
      {
        default: "$npc.name nods<<emoji 🥺>>",
        naughtyAndShy:
          "$npc.name looks a you between your legs, blushes and then nods<<emoji 😳>>",
        curiousAndShy: "=naughtyAndShy",
        shy: "$npc.name blushes and nods<<emoji 😳>>",
        curious:
          "$npc.name looks at you between your legs and then nods<<emoji 🥺>>",
        naughty: "=curious",
      },
    ],
    [
      5,
      {
        default: "say:Yes <<npcAddressPlayer>><<emoji 🙂>>",
        naughtyAndShy: `say: S-Sure <<npcAddressPlayer>>
        $npc.GenPronoun <<thirdPerson "looks" "look">> pretty excited at the idea.`,
        curiousAndShy: "=naughtyAndShy",
        shy: "say:Y-Yes <<npcAddressPlayer>><<emoji 😳>>",
        curious:
          '$npc.name takes a peek between your legs and says: "Yes <<npcAddressPlayer>>!"<<emoji 😃>>',
        naughty: "=curious",
      },
    ],
  ];
  static sleepPersonNaked = [
    [],
    [
      2,
      {
        default:
          "$npc.name looks a little surprised but nods in agreement<<emoji 🥺>>",
        naughtyAndShy:
          "$npc.name blushes like crazy sneakily putting a hand between $npc.possessive legs and then nods<<emoji 😳>>",
        shy: "$npc.name blushes like crazy and then nods<<emoji 😳>>",
        naughty: `$npc.name nods while gently stroking between $npc.possessive legs in front of you<<emoji 🥺>>
        It seems that this got $npc.pronoun excited.`,
      },
    ],
    [
      5,
      {
        default: "say:Oh... Okay... If that's what you want...",
        shy: "say:Na-Naked?!...O-Okay, if that's what you want...<<emoji 😳>>",
        naughty: "say:Yay!! Let's be naughty in bed!!<<emoji 😛>>",
      },
    ],
  ];
  static noticedBeingLookedLewdly = [
    ["lust80"],
    [
      5,
      {
        shy: `$npc.name takes a peek behind and notices your pervy gaze. $npc.GenPronoun <<thirdPerson "stops" "stop">> what $npc.genPronoun<<thirdPerson "'s" "'re">> doing while looking down and blushing heavily.`,
        naughty:
          "$npc.name notices you and suddenly spreads $npc.possessive legs without stopping $npc.possessive cooking making it easier for you to admire $npc.possessive privates.",
        default:
          `$npc.name noticed you looking but $npc.genPronoun <<thirdPerson "doesn't" "don't">> seem to mind your gaze and $npc.genPronoun <<thirdPerson "continues" "continue">> with what $npc.genPronoun<<thirdPerson "'s" "'re">> doing.`,
      },
      {
        default: "=naughty",
      },
    ],
  ];
  static receivedCookingApron = [
    ["fear40"],
    [
      5,
      {
        default: `say:Wow! Thank you sou much <<npcAddressPlayer>>!!<<emoji 🤗>>
        $npc.name is so happy that $npc.genPronoun <<thirdPerson "gives" "give">> you a hug.`,
        shy: `say:Ah!!... T-Thank you
        $npc.GenPronoun timidly <<thirdPerson "looks" "look">> at you while covering $npc.possessive reddened face.`,
        energetic: `say:Wooow!!! It's so cute!<<emoji 😃>>Thanks a lot <<npcAddressPlayer>>!!
        $npc.GenPronoun happily <<thirdPerson "jumps" "jump">> at you giving you a tight hug.`,
      },
      {
        default: `say:Ah!!... T-Thank you<<emoji 🥺>>
        $npc.name carefully takes the apron from you, a little scared.`,
      },
    ],
  ];
  static surpriseNeckKiss = [
    ["love40"],
    [
      5,
      {
        shy: "say:Eh?! W-wha?<<emoji 😳>>",
        default:
          "$npc.name let's you do your thing obediently without reacting.",
      },
      {
        default: `say:Kyaa!!<<emoji 😆>>
        $npc.GenPronoun <<thirdPerson "looks" "look">> back to smile at you<<emoji 😏>>`,
        shy: `say:Kyaa!!<<emoji 😆>>T-that tickles
        $npc.GenPronoun <<thirdPerson "says" "say">> while blushing, as $npc.genPronoun <<thirdPerson "continues" "continue">> with the cooking`,
      },
    ],
  ];
  static assGrabbed = [
    ["love40", "love60", "fear40", "fear60"],
    [
      5,
      {
        shy: `$npc.name jumps in surprise.
        <<npcSay "P-please be careful!">>`,
        naughty: `say:Tee-hee, <<npcAddressPlayer>> is grabbing my ass!!<<emoji ❤>>`,
        default: `$npc.name looks at you, not sure what to expect.`,
      },
      {
        //love40
        default: `$npc.name smiles at you while you grope $npc.pronoun.`,
        shy: "=default",
        naughty: "=default",
      },
      {
        //love60
        default: `$npc.name blushes and looks at you with a tender look.`,
        shy: "=default",
        naughty: "=default",
      },
      {
        //fear40
        default: `$npc.name jumps in surprise and looks at you looking a little worried.`,
      },
      {
        //fear60
        default: `$npc.name jumps in surprise and starts trembling.`,
      },
    ],
  ];
  static spreadPrivates = [
    [],
    [
      5,
      {
        default: "say:Ah! it's kinda chilly",
        naughty: "say:Are we gonna do lewd things?",
        shy: "say:Ah!... T-thats embarrassing <<npcAddressPlayer>>!!<<emoji 😳>>",
      },
    ],
  ];
}