import Item from "./item.js";
import Model from "./model.js";
import { sentenceCase } from "../string_utils.js";
function copy(entries) {
return (entries || []).map((entry) => ({ item: new Item(entry.item), count: entry.count }));
}
function toString(bag) {
let string = "";
if (bag.name && bag.description) {
string += `<b>${bag.name}</b> (${bag.description.trim()}): `;
} else if (bag.name) {
string += `<b>${bag.name}:</b> `;
}
let cash = 0;
if (bag.entries.length) {
let len = bag.entries.filter((entry) => entry.item.not("cash")).length;
bag.entries.forEach((entry) => {
if (entry.item.is("cash")) {
cash += entry.item.value /* 100*/ * entry.count;
} else {
string += bag.uncountable ? entry.item.toString(-1) : entry.item.toString(entry.count);
if (len === 1) {
string += ".";
} else if (len === 2) {
string += ", and ";
} else {
string += ", ";
}
len--;
}
});
if (cash) {
string += " $" + cash.toFixed(0) + " in cash.";
}
string = sentenceCase(string);
} else {
if (bag.name || bag.description) {
string += "empty.";
} else {
string += "Empty.";
}
}
return string;
}
/**
* Represents a container that holds items with quantities. Provides functionality for adding,
* removing, and querying items with support for value and encumbrance calculations. It also
* provides the facility to label the bag since many representations in the game are just
* collections of items for particular simulations.
*
* @extends Model
*/
class Bag extends Model {
/**
* Creates a new Bag instance.
*
* @constructor
* @param {Object} [data={}] - Configuration data for the bag
* @param {Array} [data.entries] - Array of bag entries with item and count properties
* @param {boolean} [data.uncountable=false] - true if the items are not counted in the
* bag (counts are ignored), false if they are counted individually
* @param {String[]} [data.tags] - Array of tags to categorize this bag
* @param {String} [data.name] - a label for the bag
* @param {String} [data.description] - details about the bag
*/
constructor(data = {}) {
super(data);
this.uncountable = data.uncountable ?? false;
this.entries = copy(data.entries);
this.name = data.name;
this.description = data.description;
this.type = "Bag";
}
/**
* Adds all items from another bag to this bag.
*
* @param {Bag} bag - The bag whose items to add
*/
addBag(bag) {
((bag && bag.entries) || []).forEach((entry) => this.add(entry.item, entry.count));
}
/**
* Removes all items from another bag from this bag.
*
* @param {Bag} bag - The bag whose items to remove
*/
removeBag(bag) {
((bag && bag.entries) || []).forEach((entry) => this.remove(entry.item, entry.count));
}
/**
* Adds items to the bag. If the item already exists, increases the count.
*
* @param {Item} item - The item to add
* @param {Number} [count=1] - The number of items to add
* @returns {Number} The new total count of this item in the bag
* @throws {Error} If no item is provided, count is negative, or item is null
*/
add(item, count = 1) {
if (!item) {
throw new Error("No item passed to bag.add()");
}
if (count < 0) {
throw new Error("Cannot add a negative number of items");
}
if (count == 0) {
return 0;
}
let entry = this.#visit(null, (e) => e.item.equals(item));
if (!entry) {
entry = { item: new Item(item), count: 0 };
this.entries.push(entry);
}
entry.count += count;
return entry.count;
}
/**
* Removes items from the bag. If count reaches zero, removes the entry entirely.
*
* @param {Item} item - The item to remove
* @param {Number} [count=1] - The number of items to remove
* @returns {Number} The new total count of this item in the bag
* @throws {Error} If count is negative, item not in bag, or trying to remove more than available
*/
remove(item, count = 1) {
if (count < 0) {
throw new Error("Cannot remove a negative number of items");
}
let entry = this.#visit(null, (e) => e.item.equals(item));
if (!entry) {
throw new Error("Can’t remove item that’s not in the bag");
}
if (count > entry.count) {
throw new Error(`Can’t remove ${count} items in bag that has only ${entry.count}`);
}
if (count == 0) {
return entry.count;
}
entry.count -= count;
if (entry.count === 0) {
this.entries.splice(this.entries.indexOf(entry), 1);
}
return entry.count;
}
typeOf(tag) {
return this.#visit(
[],
(e) => e.item.typeOf(tag),
(e, arr) => {
arr.push(e.item);
return arr;
},
);
}
/**
* Visits each entry in the bag with a predicate and collector function.
*
* @param {*} startValue - Initial value for the accumulator
* @param {Function} pred - Predicate function to test each entry
* @param {Function} [collector] - Function to collect/transform matching entries
* @returns {*} The accumulated result
*/
#visit(startValue, pred, collector = (e) => e) {
return this.entries.reduce((acc, entry) => {
if (pred(entry, acc)) acc = collector(entry, acc);
return acc;
}, startValue);
}
/**
* Finds an entry by item name or tag.
*
* @param {String} name - The name or tag to search for
* @returns {Object|null} The matching entry or null if not found
*/
find(name) {
return this.#visit(null, (e) => e.item.name === name || e.item.tags.has(name));
}
/**
* Calculates the total value of items in the bag.
*
* @param {Item} [item] - Optional specific item to calculate value for
* @returns {Number} The total value
*/
value(item) {
return this.#visit(
0,
(e) => !item || e.item.equals(item),
(e, value) => value + e.item.value * e.count,
);
}
/**
* Calculates the total encumbrance of items in the bag.
*
* @param {Item} [item] - Optional specific item to calculate encumbrance for
* @returns {Number} The total encumbrance
*/
enc(item) {
return this.#visit(
0,
(e) => !item || e.item.equals(item),
(e, sum) => sum + e.item.enc * e.count,
);
}
/**
* Calculates the total value of all items in the bag.
*
* @returns {Number} The total value of all items
*/
totalValue() {
return this.#visit(
0,
() => true,
(e, sum) => sum + e.item.value * e.count,
);
}
/**
* Calculates the total encumbrance of all items in the bag.
*
* @returns {Number} The total encumbrance of all items
*/
totalEnc() {
return this.#visit(
0,
() => true,
(e, sum) => sum + e.item.enc * e.count,
);
}
/**
* Counts the total number of items in the bag.
*
* @param {Item} [item] - Optional specific item to count
* @returns {Number} The total count of items
*/
count(item) {
return this.#visit(
0,
(e) => !item || e.item.equals(item),
(e, sum) => sum + e.count,
);
}
/**
* Converts the bag contents to a human-readable string with proper pluralization.
*
* @returns {String} Human-readable description of bag contents
*/
toString() {
return toString(this);
}
}
export default Bag;