create_kit.js

import { createGender } from "./create_character.js";
import { createItem } from "./create_item.js";
import { createProfession, getPostProfessions } from "./create_profession.js";
import { logger } from "./utils.js";
import { roll, random, shuffle, test } from "./random_utils.js";
import { TRAITS as T } from "./constants.js";
import Bag from "./models/bag.js";

const looseAmmo = {
  common: "8d8",
  uncommon: "4d8",
  rare: "3d6",
};

function addAmmo(bag, firearm) {
  const ammoType = firearm.consumes;
  const bundledAmmo = createItem({ tags: `ammo ${ammoType} bundled` });
  if (test(20) && bundledAmmo) {
    bag.add(bundledAmmo, roll("2d3"));
  } else {
    const ammo = createItem({ tags: `ammo ${ammoType} -bundled` });
    bag.add(ammo, roll(looseAmmo[ammo.frequency]));
  }
  if (firearm.is("pistol") && test("60")) {
    const holster = createItem({ tags: `holster` });
    bag.add(holster);
  }
}

function add(bag, tags, count = 1) {
  for (let i = 0; i < count; i++) {
    let item = createItem({ tags });
    if (item) {
      bag.add(item, 1);
      return true;
    }
    return false;
  }
}

function addUntil(bag, ...allTagArrays) {
  shuffle(allTagArrays);
  for (let i = 0; i < allTagArrays.length; i++) {
    let tagArray = allTagArrays[i];
    let haveAdded = false;
    for (let j = 0; j < tagArray.length; j++) {
      let item = createItem({ tags: tagArray[j] });
      if (item) {
        bag.add(item, 1);
        haveAdded = true;
      }
    }
    if (haveAdded) {
      return;
    }
  }
}

/**
 * Generate the possessions that would be on the person of an active NPC (e.g.
 * out on patrol, out for a night on the town, out on a raid or in the middle
 * of criminal activity).
 *
 * @param params {Object}
 *   @param [params.profession] {String|Profession} profession name or instance
 *   @param [params.preProfession] {String|Profession} profession name or instance
 *   @param [params.gender] {String} gender of character (male or female)
 *   @param [params.traveling] {Boolean} should this bag include extended equipment
 *    for exploring, and not just walk-abouts?
 *
 * @return {Object} An object of two bags, one under the property "clothing" and another
 *    under the bag of "possessions".
 */
export function createKit({
  profession = random(getPostProfessions()),
  gender = createGender(),
  traveling = false,
} = {}) {
  logger.start("createKit", { profession, gender, traveling });

  const clothing = new Bag();
  const possessions = new Bag();
  const selGender = gender.toLowerCase();
  const profConfig = createProfession({ name: profession });
  const kit = profConfig.kit;

  addUntil(
    clothing,
    [`clothing body -armor kit:${kit} (${selGender} | unisex)`],
    [
      `clothing torso -armor -coat kit:${kit} (${selGender} | unisex)`,
      `clothing legs -armor kit:${kit} (${selGender} | unisex)`,
    ],
    [`clothing body -armor -kit (${selGender} | unisex)`],
    [
      `clothing torso -armor -coat -kit (${selGender} | unisex)`,
      `clothing legs -armor -coat -kit (${selGender} | unisex)`,
    ],
  );
  // shoes
  addUntil(
    clothing,
    [`clothing feet -kit (${selGender} | unisex)`],
    [`clothing feet kit:${kit} (${selGender} | unisex)`],
  );
  // hat
  if (test(80)) {
    addUntil(
      clothing,
      [`clothing head -armor -kit (${selGender} | unisex)`],
      [`clothing head -armor kit:${kit} (${selGender} | unisex)`],
    );
  }

  // weapons and ammo. people don't carry broken weapons
  const condition = random(["fair", "good", "good", "good", "excellent"]);
  const firearm = createItem({ tags: `firearm`, condition });
  const melee = createItem({ tags: `melee` });
  const result = roll("1d10");
  if (result < 4) {
    possessions.add(firearm);
    addAmmo(possessions, firearm);
  } else if (result < 8) {
    possessions.add(melee);
    possessions.add(firearm);
    addAmmo(possessions, firearm);
  } else {
    possessions.add(melee);
  }
  if (firearm.is("rifle") && test(15)) {
    possessions.add(createItem({ tags: "rifle-scope" }));
  }
  if (!profConfig.tags.has("prestige:high") && test(20)) {
    addUntil(possessions, [`armor head -kit`]);
    addUntil(possessions, [`armor (body | torso) -kit`]);
  }

  // criminal, cowboy, science, military, worker, doctor, guard, gang, scavenger
  if (kit) {
    add(possessions, `kit:${kit}`);
  }
  if (traveling) {
    // TODO: separate trinkets and accessories, accessories go under clothing
    add(possessions, `accessory`, roll("1d4-2"));

    add(possessions, `food -candy (preserved | ration | fruit)`, roll("2d3"));

    add(possessions, `travel`, roll("1d4-1"));
    // Not entirely happy with the horse as a possession. The horse can carry stuff and probably
    // would be carrying some stuff like feed, there’s no way to describe this, nor the relationship
    // to the character. What if they have two horses, or six?
    createTransport({ bag: possessions, profConfig });
  }
  return logger.end({
    clothing,
    possessions,
    gender: selGender,
    profession: random(profConfig.names),
  });
}

function hasTrait(prof, trait) {
  return prof.seeds.includes(trait) || prof.traits.includes(trait);
}

function createTransport({ bag, profConfig } = {}) {
  const chanceHorse = hasTrait(profConfig, T.HORSEBACK_RIDING) ? 70 : 20;
  const chanceMotorcycle = hasTrait(profConfig, T.MOTORCYCLING) ? 30 : 5;
  console.log("chanceHorse", chanceHorse, "chanceMotorcycle", chanceMotorcycle);

  // TODO: If they have a motorcycle, they have some amount of gas and gas in tanks.
  const hasHorse = test(chanceHorse);
  const hasMotorcycle = test(chanceMotorcycle);

  if (chanceHorse > chanceMotorcycle && hasHorse) {
    add(bag, "horse");
    if (test(80)) {
      add(bag, `horse-tack`);
      add(bag, `saddle`);
      if (test(80)) {
        add(bag, `saddlebag`, roll("1d2"));
      }
    }
  } else if (hasMotorcycle) {
    add(bag, "motorcycle");
  }
}