create_bag.js

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

/**
 * 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.
 *
 * @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 = 20,
  enc = Number.MAX_VALUE,
  count = Number.MAX_VALUE,
  uncountable = false,
  minValue = 0,
  tags = "*",
} = {}) {
  if (typeof value === "string") {
    value = roll(value);
  }
  logger.start("createBag", { tags, value, repeat, enc, count, uncountable });

  const bag = new Bag({ uncountable });
  const visitedItems = new Map();

  let currentValue = value;
  let currentEnc = enc;
  let currentCount = count;
  let safetyLoops = count === Number.MAX_VALUE ? 70 : count * 10;
  let item = null;

  outer: while (currentValue > 0 && currentEnc > 0 && currentCount > 0 && safetyLoops-- > 0) {
    if (!test(repeat) || item === null) {
      do {
        item = createItem({ tags, minValue, maxEnc: currentEnc, maxValue: currentValue });
        if (item == null) {
          break outer; // criteria no longer match
        }
      } while (visitedItems.get(item.name) && safetyLoops-- > 0);
    }
    if (item !== null) {
      visitedItems.set(item.name, true);
      bag.add(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);
}