import Model from "./model.js";
import Item from "./item.js";
import { sentenceCase, pluralize, uncountable } from "../string_utils.js";
/**
* Converts a bag's contents to a human-readable string representation.
*
* @private
* @param {Bag} bag - The bag to convert to string
* @param {Function} pluralizerFunc - Function to pluralize item names
* @returns {String} Human-readable description of bag contents
*/
function toString(bag, pluralizerFunc) {
let string = "",
cash = 0;
if (bag.entries.length) {
let items = false;
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 {
items = true;
string += pluralizerFunc(entry.item, entry.count);
if (entry.item.title) {
string += ` (${entry.item.title})`;
}
if (len === 1) {
string += ".";
} else if (len === 2) {
string += ", and ";
} else {
string += ", ";
}
len--;
}
});
if (items && cash) {
string += " ";
}
if (cash) {
string += "$" + cash.toFixed(0) + " in cash.";
}
string = sentenceCase(string);
} else {
string += "empty.";
}
if (bag.descriptor) {
string = bag.descriptor + ": " + string;
}
return string;
}
/**
* Creates a deep copy of bag entries array.
*
* @private
* @param {Array} entries - Array of bag entries to copy
* @returns {Array} Deep copy of the entries array
*/
function copy(entries) {
return (entries || []).map((entry) => ({ item: new Item(entry.item), count: entry.count }));
}
/**
* Represents a container that holds items with quantities. Provides functionality
* for adding, removing, and querying items with support for value and encumbrance calculations.
*
* @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 {String[]} [data.tags] - Array of tags to categorize this bag
*/
constructor(data = {}) {
super(data);
this.entries = copy(data.entries);
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, value) => value + 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, value) => value + 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, value) => value + 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, value) => value + e.count
);
}
/**
* Converts the bag contents to a string using uncountable item names.
*
* @returns {String} Human-readable description with uncountable names
*/
toUncountableString() {
return toString(this, uncountable);
}
/**
* Converts the bag contents to a human-readable string with proper pluralization.
*
* @returns {String} Human-readable description of bag contents
*/
toString() {
return toString(this, pluralize);
}
}
export default Bag;