create_bag.js

import { createItem } from "./create_item.js";
import { getCondition } from "./condition.js";
import { logger } from "./utils.js";
import { random, roll, test } from "./random_utils.js";
import Bag from "./models/bag.js";

const SILENCER_WIELDERS = ["Courier", "Scavenger"];

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

// There are items that have dependencies that are not easily modeled elsewhere
export function augment(bag, item, profession) {
  let count = item.is("multi") ? roll("3d8-2") : 1;
  bag.add(item, count);

  if (item.not("firearm")) {
    return;
  }
  // firearms come with ammos, silencers, scopes
  const ammoType = item.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(LOOSE_AMMO[ammo.frequency]));
  }
  const chanceSilencer = SILENCER_WIELDERS.includes(profession) ? 50 : 10;
  if (test(chanceSilencer)) {
    const silencer = createItem({ tags: `silencer ${item.consumes}` });
    if (silencer) {
      bag.add(silencer);
    }
  }
  if (test(15) && item.is("rifle")) {
    bag.add(createItem({ tags: "rifle-scope" }));
  }
  if (test(60) && profession && item.is("pistol")) {
    bag.add(createItem({ tags: `holster` }));
  }
}

/**
 * Creates a collection of objects, held in a collection type known as a Bag.
 *
 * @param [params] {Object}
 *   @param [params.value=10] {Number | String} the total value of items to add to the bag.
 *     The method will get as close to this value as possible, but may go over. If the
 *     value is a string, it is treated as dice notation.
 *   @param [params.repeat=20] {Number} This is the percentage chance that the last item
 *     selected will be added again. Can range from 0% (no repeated items) to 100% (only
 *     one item, as many as are needed to reach the other specified limits).
 *   @param [params.count=Number.MAX_VALUE] {Number} affects how many items will be added to
 *     the bag. The bag will not exceed this number of items.
 *   @param [params.enc=Number.MAX_VALUE] {Number} the maximum encumbrance of the bag.
 *     The method will get as close to this value as possible, but may go over.
 *   @param [params.uncountable=false] {Boolean} if true, items in the bag are treated as
 *     uncountable.
 *   @param [params.minValue=0] {Number} the minimum value of any items added to the bag (for
 *     some applications, like store stock, it is desirable to set this number above 0).
 *   @param [params.tags="*"] {String} tags that will be used to search for items to add to the bag.
 *   @param [params.name] {String} a name for the bag.
 *   @param [params.description] {String} a description for the bag.
 *   @param [params.condition] {String} if the item selected has the “br” tag and thus has
 *     condition, you can fix it’s condition by passing it in to this method.
 *
 * @example
 * createBag({ count: 3, tags: "house"}).toString()
 * => "A television (5/20), a winter jacket (worn; 0.5/10), and a ceramic bowl (3/1)."
 *
 * @returns {Bag} a bag containing items that meet the specified criteria
 */
export function createBag({
  value = 10,
  repeat = 5,
  enc = Number.MAX_VALUE,
  count = Number.MAX_VALUE,
  uncountable = false,
  minValue = 0,
  tags = "*",
  name,
  description,
  conditions,
} = {}) {
  if (typeof value === "string") {
    value = roll(value);
  }
  logger.start("createBag", {
    tags,
    value,
    repeat,
    enc,
    count,
    uncountable,
    name,
    description,
    conditions,
  });

  const bag = new Bag({ uncountable, name, description });
  const visitedItems = new Set();

  let currentValue = value;
  let currentEnc = enc;
  let currentCount = count;
  let item = null;

  outer: while (currentValue > 0 && currentEnc > 0 && currentCount > 0) {
    if (!test(repeat) || item === null) {
      let safetyLoops = count === Number.MAX_VALUE ? 200 : count * 50;
      do {
        let thisCondition = conditions ? random(conditions) : getCondition().name;
        item = createItem({
          tags,
          minValue,
          maxEnc: currentEnc,
          maxValue: currentValue,
          condition: thisCondition,
        });
        if (item == null) {
          break outer; // criteria no longer match
        }
      } while (visitedItems.has(item.name + item.title) && safetyLoops-- > 0);
    }
    if (item !== null) {
      visitedItems.add(item.name + item.title);
      augment(bag, item);

      currentCount--;
      // if item.value == 0 (or enc), decrement by a random value or you'll get as many
      // items as there are safety loops (unless count or enc is set).
      currentValue -= item.value || roll(5);
      currentEnc -= item.enc || roll(5);
    }
  }
  return logger.end(bag);
}