import { roll, test } from "./random_utils.js";
import { createItem } from "./create_item.js";
import { logger } from "./utils.js";
import Bag from "./models/bag.js";
// Uses the existing API to get the right frequency of objects, but can lead to many duplicates.
// of all the fill* methods, this is the only one that respects all the createBag() options,
// the rest are specific to creating a believable kit.
function fill(bag, opts) {
let bagValue = opts.totalValue;
let escape = 0;
while (bagValue > 0.99) {
escape++;
if (escape === 1000) {
break;
}
// Take the the max value or the bag value, unless they are less than the min value,
// then take the min value. For that reason, the item's value has to be tested below
// to verify it is less than the remaining bag value (if it's not, just quit and return
// the bag).
opts.maxValue = Math.max(Math.min(opts.maxValue, bagValue), opts.minValue);
// Because maxValue changes every query, we can't cache any of this.
const item = createItem(opts);
if (item === null || item.value > bagValue) {
return bag;
}
bag.add(item);
if (bag.totalEnc() > opts.totalEnc) {
bag.remove(item);
break;
}
bagValue -= item.value;
}
return bag;
}
function getClusterSpread(value) {
switch (value) {
case "low":
return roll("2d3+5"); // 7-11
case "medium":
return roll("2d3+2"); // 4-8
case "high":
return roll("1d3+2"); // 3-5
}
}
function getClusterCount(value) {
switch (value) {
case "low":
return roll("1d3*2"); // 2-6
case "medium":
return roll("(1d3*2)+2"); // 3-8
case "high":
return roll("(1d3*2)+4"); // 6-10
}
}
function fillCurrency(bag, amount) {
while (amount > 0) {
const currency = createItem({ tags: "currency", maxValue: amount });
if (currency === null) {
return;
}
bag.add(currency);
amount -= currency.value;
}
}
function kitUniques(bag, count, tags) {
let set = new Set();
for (let i = 0; i < count; i++) {
let item = createItem({ tags });
if (item) {
set.add(item);
}
}
setToBag(bag, set);
}
function setToBag(bag, set) {
Array.from(set).forEach((item) => bag.add(item));
return bag;
}
/**
* Generate a collection of items.
*
* @example
* let bag = createBag({ totalValue: 500, minValue: 10, tags: 'firearm'});
* bag.toString()
* => "2 Browning Automatic Rifles, a M14 Rifle...and a pulse rifle."
*
* @param [params] {Object}
* @param [params.tags] {String} One or more query tags specifying the items in the bag
* @param [params.minValue] {Number}
* @param [params.maxValue] {Number}
* @param [params.minEnc] {Number}
* @param [params.maxEnc] {Number}
* @param [params.totalValue=20] {Number} The total value of the bag
* @param [params.fillBag=true] {Boolean} Should the bag's value be filled with
* currency if it hasn't been filled any other way? Usually currency has a value of
* 1 or less, and can cover a gap otherwise caused by search criteria, but this
* isn't always desirable.
*/
export function createBag({
tags = "*",
totalValue = 20,
fillBag = true,
minValue = 0,
maxValue = Number.MAX_VALUE,
minEnc = 0,
maxEnc = Number.MAX_VALUE,
totalEnc = Number.MAX_VALUE,
} = {}) {
logger.start("createBag", { tags, totalValue, minValue, maxValue, minEnc, maxEnc, totalEnc });
if (maxValue <= 0 || maxEnc <= 0 || minValue > maxValue || minEnc > maxEnc) {
throw new Error(
"Conditions cannot match any taggable: " +
JSON.stringify({ minValue, maxValue, minEnc, maxEnc })
);
}
if (typeof totalValue <= 0) {
throw new Error("Bag value must be more than 0");
}
let bag = fill(new Bag(), { tags, totalValue, minValue, maxValue, minEnc, maxEnc, totalEnc });
if (fillBag !== false) {
fillCurrency(bag, totalValue - bag.value());
}
return logger.end(bag);
}
/**
* Like creating a bag but with many more repeated items (purposefully repeated, not
* accidentally repeated), as if collected for a cache, shop, or storeroom. Honors the
* `totalValue` limit (in fact will usually fall short of it), but `fillBag` will always be
* treated as false.
*
* @param [params] {Object}
* @param [params.tags] {String} One or more query tags specifying the items in the bag
* @param [params.cluster="medium"] {String} "low", "medium" or "high". Alters the
* amount of stockpiling from a little to a lot.
* @param [params.minValue] {Number}
* @param [params.maxValue] {Number}
* @param [params.minEnc] {Number}
* @param [params.maxEnc] {Number}
* @param [params.totalValue=20] {Number} The total value of the stockpile. In practice,
* the stockpile will be worth less than this. Must be at least 20.
*
* @returns {Bag} a bag representing a stockpile
*/
export function createStockpile({
tags = "*",
cluster = "medium",
fillBag = false,
totalValue = 20,
minValue = 0,
maxValue = Number.MAX_VALUE,
minEnc = 0,
maxEnc = Number.MAX_VALUE,
} = {}) {
logger.start("createStockpile", {
tags,
cluster,
fillBag,
totalValue,
minValue,
maxValue,
minEnc,
maxEnc,
});
if (cluster === "none") {
let bag = createBag({ tags, totalValue, fillBag, minValue, maxValue, minEnc, maxEnc });
return logger.end(bag);
}
let bag = new Bag();
let count = getClusterSpread(cluster);
kitUniques(bag, count, "-currency " + tags);
bag.entries.forEach(function (entry) {
count = getClusterCount(cluster);
bag.add(entry.item, count);
});
return logger.end(bag);
}
/**
* A simplified bag creation algorithm. Create a bag by iteratively adding items until
* constraints are met.
*
* @param [params] {Object}
* @param [params.value=20] {Number | String} the total value of items to add to the bag.
* The method will get as close to this value as possible. If the value is a string,
* it is treated as dice notation.
* @param [params.repetition=20] {Number} affects how many times items can repeat.
* This is the percentage chance that the last item selected will be added again, unless
* it would exceed the value or encumbrance limits.
* @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 it's not guaranteed.
* @param [params.uncountable=false] {Boolean} if true, items in the bag are treated as
* uncountable.
* @param [params.tags="*"] {String} tags to search for items to put in the bag.
*
* @returns {Bag} a bag containing items that meet the specified criteria
*/
export function newCreateBag({
value = 20,
repetition = 20,
enc = Number.MAX_VALUE,
uncountable = false,
tags = "*",
} = {}) {
if (typeof value === "string") {
value = roll(value);
}
logger.start("newCreateBag", { tags, value, repetition, enc, uncountable });
let bag = new Bag({ uncountable });
let currentValue = value;
let currentEnc = enc;
let lastItem = null;
let loops = 0;
while (currentValue > 0 && currentEnc > 0 && loops++ < 50) {
let item =
test(repetition) && lastItem !== null
? lastItem
: createItem({ tags, maxEnc: currentEnc, maxValue: currentValue });
if (item === null) {
break;
}
// stores (uncountable) don't sell things of no value...it's a weird edge case
if (item.value === 0 && uncountable) {
continue;
}
lastItem = item;
bag.add(item);
currentValue -= item.value;
currentEnc -= item.enc;
}
return logger.end(bag);
}