
*** Look into whether <<nobr>> or the "nobr" tag break how <<UInvSet>> works.

*** Make a way to add static items by type and/or tag?

*** Update linear-gradient stuff:
	background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #2D2D2D), color-stop(100%, #6b6b6b));
	background-image: -ms-linear-gradient(top, #C4CACC 0%, #2D2D2D 2px, #6b6b6b 100%);
	background-image: -o-linear-gradient(top, #C4CACC 0%, #2D2D2D 2px, #6b6b6b 100%);
	background-image: -moz-linear-gradient(top, #C4CACC 0%, #2D2D2D 2px, #6b6b6b 100%);
	background-image: -webkit-linear-gradient(top, #C4CACC 0%, #2D2D2D 2px, #6b6b6b 100%);
	background-image: linear-gradient(to bottom, #C4CACC 0%, #2D2D2D 2px, #6b6b6b 100%);


Universal Inventory System (UInv)
=================================

♦ fully unit tested and regression test available
+ written and tested
* written and tested, but incomplete
/ written, but incomplete and not tested
- written, but not tested
x needs to be updated to handle pockets properly
A needs to be modified to support ItemTag arrays


Bag functions:  (BagName must be unique.  BagPropertyName must be unique for its bag.)
--------------

+ AddBag(BagName, [DefaultBagType] {, StartDepth, CurrentDepth})
♦ AddToBagPropertyValue(BagName, BagPropertyName, Amount)
♦ BagExists(BagName/Array) = t/f
- BagHasAllProperties(BagName, BagPropertyNameArray) = t/f if all of the bag's properties are listed in BagPropertyNameArray
♦ BagHasProperty(BagName, BagPropertyName/Array) = t/f
- BagMatchesDefault(BagName) = t/f depending on whether it is an exact match for its default bag type
♦ BagPropertyCount(BagName) = number of BagName's properties, or undefined if there was an error.
- CopyAllItemsToBag(SourceBagName, DestinationBagName) Copies all items from source to destination.
+ CopyBag(ExistingBagName, NewBagName)
♦ CopyBagProperty(SourceBagName, DestinationBagName, BagPropertyName/Array) overwrites existing properties
♦ CreateBag(BagName)  (Sets UInvTouched to false)
+ DeleteBag(BagName/Array)
♦ DeleteBagProperty(BagName, BagPropertyName)
♦ EmptyBag(BagName)
♦ GetBagByProperty(BagPropertyName) = get a random BagName that has property BagPropertyName
♦ GetBagCount() = the number of bags
♦ GetBagCountByDefaultType() = the number of unique bag types, bags with no default bag type count as unique bag types
+ GetBagObject(BagName) = the full bag object (including UInvDefaultBagType)
♦ GetBagPropertyArray(BagName) = array of all properties a bag has.
♦ GetBagPropertyObject(BagName) = object of all properties/values a bag has.
♦ GetBagPropertyValue(BagName, BagPropertyName)        [alias: GetBagProp] !!! add more aliases !!!
♦ GetBagsArrayByProperty(BagPropertyName, [BagNameArray]) = array of BagNames that have property BagPropertyName (within the set of BagNameArray if parameter is included)
♦ GetBagsArray() = an array of all bag names   [alias: BagArray]
♦ GetBagsArrayWherePropertyEquals(BagPropertyName, Value) = array of BagNames where BagPropertyName value == Value
♦ GetBagsArrayWherePropertyGreaterThan(BagPropertyName, Value) = array of BagNames where BagPropertyName value > Value
♦ GetBagsArrayWherePropertyLessThan(BagPropertyName, Value) = array of BagNames where BagPropertyName value < Value
- GetBagsArrayWithAllProperties(BagPropertyNameArray) = array of all bags which have all of the properties in BagPropertyNameArray (per the ArrayHasAllBagProperties function)
- GetBagsArrayWithItem(ItemName, [BagArray]) = array of BagNames that have item (limited to items in BagArray bags if BagArray is passed to function)
♦ GetBagsDefaultType(BagName)
♦ GetBagWherePropertyEquals(BagPropertyName, Value) = BagName  (get a random BagName where BagPropertyName == Value)
♦ GetBagWherePropertyGreaterThan(BagPropertyName, Value) = BagName  (get a random BagName where BagPropertyName > Value)
♦ GetBagWherePropertyLessThan(BagPropertyName, Value) = BagName  (get a random BagName where BagPropertyName < Value)
♦ GetBagWithHighestPropertyValue(BagPropertyName, [BagNameArray]) = BagName (bags without BagPropertyName are ignored); randomly pick one of the highest if multiple bags are tied for highest
♦ GetBagWithLowestPropertyValue(BagPropertyName, [BagNameArray])  = BagName (bags without BagPropertyName are ignored); randomly pick one of the lowest if multiple bags are tied for lowest
♦ GetCurrentBagName() Gets the current bag name if there is one, otherwise returns "".
+ GetDefaultBagObject(DefaultBagType, [PropertiesOnly]) (if PropertiesOnly is true, only return the default properties)
- GetTotalBagPropertyValue(BagPropertyName, [BagNameArray]) gets the total value of BagPropertyName across all bags, or across all bags in BagNameArray
+ GetUniqueBagName() returns a name not used by any bag currently ("bag0ffd39" 0-255 in hex * 3)
- MergeBags(SourceBagName, DestinationBagName) Moves all items from source to destination and deletes source.
- MoveAllItemsToBag(SourceBagName/Array, DestinationBagName) Moves all items from source(s) to destination.
- MoveBagPropertyValueToBag(SourceBagName, SourceBagPropertyName, DestinationBagName, [DestinationBagPropertyName], [Amount], [MinimumValue], [MaximumValue], [DeletionValue], [DeletionType])
- MoveItemPropertyValueToBag(SourceBagName, SourceItemName, SourceItemPropertyName, DestinationBagName, [DestinationBagPropertyName], [Amount], [MinimumValue], [MaximumValue], [DeletionValue], [DeletionType])
+ RenameBag(CurrentBagName, NewBagName)
♦ SetCurrentBagName(BagName)
♦ SetBagPropertyValue(BagName/Array, BagPropertyName, Value) [alias: SetBagValue] add property if it doesn't exist
♦ SetBagsDefaultType(BagName, DefaultBagType)
♦ SetBagTouched(BagName/Array) sets bag(s) as touched.
♦ SetBagUntouched(BagName/Array) sets bag(s) as untouched.
♦ WasTouched(BagName) = Whether items in bag have changed since creation or since Untouched was last set.

Complete!!!  (49 functions)

Add item blacklists and whitelists?
Add maximum capacities? (the sum of property X on all items maxes out at Y, then no more may be added)
Add some sort of "Try" functionality to support the two above items?
*** Do the following in more BagName/Array functions to eliminate wasting time on duplicate bags???:
BagName = UInv.getUniqueArray(BagName);



Pocket/Container functions:  (These are bags/pockets which are "inside" items/containers.)
---------------------------

- AddExistingBagAsPocket(BagName, ItemName, PocketName, PocketBagName) (avoids recursion)
+ AddPocket(BagName, ItemName, PocketName, [DefaultBagType] {, StartDepth, CurrentDepth}) adds a pocket named PocketName of a particular bag type to an item
- BagHasSpecificItem(MainBagName, ItemBagName, ItemName) = whether MainBagName contains ItemBagName+ItemName, or undefined on error.  (Only needed if you're using pockets.)
- BagIsPocket(BagName) = number of items it's a pocket for (uses GetPocketContainerArray)
- ContainerHasItem(ContainerBagName, ContainerName, ItemName) = number of items named ItemName in all of container's pockets (does not include the container itself in that count)
- ContainerHasPocketBag(ContainerBagName, ContainerName, BagName) = returns whether BagName exists anywhere within the container, or undefined on error
- CreatePocket(BagName, ItemName, PocketName) creates an empty pocket named PocketName attached to an item
- DeletePocket(BagName, ItemName, PocketName) deletes the pocket named PocketName on an item
- GetAllBagPockets(BagName/Array, [NoSourceBag]) = array of all pockets' and sub-pockets's BagNames, plus BagName (unless NoSourceBag == true), or undefined on error  (all BagNames returned in the array will be unique within that array)
- GetAllContainerPockets(ContainerBagName, ContainerName) = array of all pockets' and sub-pockets's BagNames, or undefined on error  (all BagNames returned in the array will be unique within that array)
- GetContainerIndex(BagName, PocketName) = Index of container in a bag's UInvContainer array, -1 if not found, -999 on error.
- GetItemPocketBagArray(BagName, ItemName) = an array of bag names which the item has as pockets, returns [] if it has none, or undefined on error.
+ GetItemPocketBagName(BagName, ItemName, [PocketName]) = bag name of PocketName in an item, or a random pocket bag name if PocketName isn't specified
- GetItemPocketNameArray(BagName, ItemName) = an array of pocket names which the item has as pockets, returns [] if it has none, or undefined on error.
- GetItemPocketObject(BagName, ItemName) = an object of { "PocketName": "BagName", ... } from item
- GetPocketBagContainerArray(PocketBagName) = array of { BagName : "name", ItemName : "name", PocketName : "name" } -or- [] if it's not a pocket
- GetPocketBagsPocketName(BagName, ItemName, PocketBagName) = PocketName
- GetPocketDepth(BagName) = 0 if it's a bag, 1 if it's a pocket, 2 if it's a pocket within a pocket, etc... or undefined on error.
+ ItemHasPocket(BagName, ItemName, [PocketName]) = number of pockets directly on item (uses GetItemPocketArray) -or- if item has a pocket named PocketName (if the PocketName parameter is set)
- MoveItemFromPocket(ContainerBagName, ContainerItemName, PocketName, ItemName, DestinationBagName, [Quantity])
+ MoveItemToPocket(SourceBagName, SourceItemName, DestinationBagName, DestinationItemName, DestinationPocketName, [Quantity])
- MovePocket(SourceBagName, SourceItemName, SourcePocketName, DestinationBagName, DestinationItemName, [DestinationPocketName])
- UnlinkPocketBagFromContainer(BagName, ContainerName, PocketBagName) Unlinks a bag from the given container item.
- UnlinkPocketFromContainer(BagName, ContainerName, [PocketName]) Unlinks a pocket from its container item, or all pockets if PocketName isn't set.

In pocket/container functions:  ("pockets" are bags connected to "containers", "containers" are items that are connected to "pockets")
 • the UInvQuantity must be 1 to have a pocket, if it's stacked it will become unstacked when a pocket is added
 • items with pockets do not stack and cannot be merged
 • recursive pockets are not allowed (you can't put a container inside its own pocket, directly or indirectly)
 • automatically handles deleted containers and pockets
 • checking the contents of bags should also check item pockets (?)
 • getting something from a bag should check pockets if it's not in the bag itself (?)
 • "hidden" tags on pockets?  (So that a list of all items won't include/affect items in hidden pockets.)
 • check all functions to make sure the relationship between containers and pockets aren't broken
 • mark bags as touched if added/removed pockets have any contents
 • prevent created items and bags from having pockets greater depth than MaximumPocketDepth  (i.e. keep a suit item from having a default pocket which has a suit in it, and that suit has a pocket with a suit in it, ... ad infinitum)

Item gets: UInvPocket object = { "PocketName": "BagName", ... }
Bag gets: UInvContainer array = [ { ContainerBagName: "name", ContainerName: "name", PocketName: "name" }, ... ]

(Trigger bag "touched" events?  Should "touched" events trickle up from pockets?)
(How to deal with quantities in AddItem and CopyItem?)
(Add find by DefaultType and Property as well? - Make sure existing functions can do this?)
(Figure out a way to combine items that all have empty pockets?)

*** Make sure that UInvQuantity can't be increased on items with pockets

Complete!!!  (24 functions)



Item functions:  (ItemName must be unique within a bag and in the GetDefaultItemObject function.)
---------------

+ AddItem(BagName, ItemType, [Quantity], [NewItemName] {, StartDepth, CurrentDepth}) pulls item from GetDefaultItemObject, default Quantity = 1
AddItemCapped(BagName, ItemType, [MaxItems], [Quantity], [NewItemName]) adds items up to MaxItems; default MaxItems and Quantity = 1; returns current item quantity
- AddItems(BagName, ItemArray)
- AddToAllItemsPropertyValue(BagName, ItemPropertyName, Amount) adds amount to property's value for all items that have ItemPropertyName
+ AddToItemPropertyValue(BagName, ItemName, ItemPropertyName, Amount) adds amount to property's value
- BagHasAllItems(BagName/Array, ItemNameArray) = t/f if bag/s has/have all of the items in ItemNameArray
- BagHasAnyItem(BagName/Array, ItemNameArray) = t/f if bag/s has/have any of the items in ItemNameArray
+ BagHasItem(BagName/Array, ItemName) = Quantity  [alias: GetItemQuantity, HasItem, ItemQuantity]
+ CopyItem(SourceBagName, DestinationBagName, ItemName, [Quantity], [NewItemName]) Quantity = amount to add, default to same quantity, NewItemName must be used if source and destination bags are the same
- CopyItemsByProperty(SourceBagName, DestinationBagName, ItemProperty, [Value]) copy all items in bag that have the ItemProperty, or ItemProperty === Value if Value is passed
- CreateItem(BagName, ItemName, [Quantity])  create new item instead of pulling from GetDefaultItemObject function
+ DeleteItem(BagName, ItemName/Array, [Quantity])  if no Quantity then remove all.
+ DeleteItemProperty(BagName/Array, ItemName, [ItemPropertyName]) cannot delete ItemPropertyName "UInvQuantity", "UInvDefaultItemType", or "UInvPocket", also deleting a property of a default item will return that property to its default value
- DeleteItemsByProperty(BagName, ItemProperty, [Value]) delete all items in bag that have the ItemProperty, or ItemProperty === Value if Value is passed
- GetAllPropertyValues(BagName, ItemPropertyName) = [ array of all unique values that property has ]
- GetBagItemArrayWhereItemPropertyEquals(BagNameArray, ItemPropertyName, Value) = array of [[BagName, ItemName], ...] where the item's ItemPropertyName property == Value
+ GetCurrentItemName(ItemName) Gets the current item name if there is one, otherwise returns "".
+ GetDefaultItemObject(ItemType) = item object
- GetDefaultItemPropertyValue(ItemName, ItemPropertyName) = default value for item's property
- GetItemByProperty(BagName, ItemPropertyName) = get a random ItemName that has property ItemPropertyName
+ GetItemByType(BagName, ItemType) = get a random ItemName that is of type ItemType
+ GetItemCount(BagName/Array) = the number of unique items in each bag (ignores Quantity)
- GetItemCountByDefaultType(BagName/Array) = the number of unique item types in each bag (ignores Quantity), items with a default item type of "-" are each counted as separate unique item types
- GetItemCountByFunction(BagName/Array, CountFunction) = returns the sum of the values returned by function (function is passed BagName)
- GetItemCountFull(BagName/Array) = the total number of items in bag(s) (Quantity included)
- GetItemCountFullByDefaultType(BagName/Array, DefaultItemType) = the total number of items in bag(s) (Quantity included) of that DefaultItemType
- GetItemCountWherePropertyEquals(BagName/Array, ItemPropertyName, Value)
- GetItemCountWherePropertyGreaterThan(BagName/Array, ItemPropertyName, Value)
- GetItemCountWherePropertyLessThan(BagName/Array, ItemPropertyName, Value)
+ GetItemObject(BagName, ItemName) = item object
+ GetItemPropertiesArray(BagName, ItemName) = Array of all properties on item in bag
+ GetItemPropertyCount(BagName, ItemName) = number of ItemPropertyNames ItemName has (inc. quantity)
- GetItemPropertyValue(BagName, ItemName, ItemPropertyName) = Value, undefined if PropertyName doesn't exist
- GetItemPropertyValueObject(BagName, ItemPropertyName) = { ItemName : ItemPropertyName's value, ... } (items that don't have ItemPropertyName are ignored)
- GetItemsAndQuantitiesObject(BagName) = { item1: quantity1, item2: quantity2, etc... } in a bag
+ GetItemsArray(BagName) = array of item objects' names
- GetItemsArrayByFunction(BagName, SelectionFunction) = array of items where function is true (function is passed BagName and ItemName strings)
- GetItemsArrayByProperty(BagName, ItemPropertyName) = array of ItemNames that have property ItemPropertyName
- GetItemsArrayByType(BagName, ItemType) = array of ItemNames that are of type ItemType
- GetItemsArraySortedByFunction(BagName, SortFunction) returns an array of ItemNames sorted by the function.
- GetItemsArraySortedByProperty(BagName, [ItemPropertyName]) returns an array of ItemNames sorted by the items' names unless ItemPropertyName parameter is used, in which case it's sorted by that property's value (if the property exists; only sorting by numbers and strings are supported for that)
- GetItemsArrayWhereItemNameContains(BagName, SubString) = array of ItemNames where item's name contains the substring; use RegExp matching?
- GetItemsArrayWherePropertyEquals(BagName, ItemPropertyName, Value) = array of ItemNames where property == Value
- GetItemsArrayWherePropertyGreaterThan(BagName, ItemPropertyName, Amount) = array of ItemNames where property value > Amount
- GetItemsArrayWherePropertyLessThan(BagName, ItemPropertyName, Amount) = array of ItemNames where property value < Amount
- GetItemsArrayWherePropertyValueContains(BagName, ItemPropertyName, SubString, [CaseSensitive]) = array of ItemNames where property contains the substring; CaseSensitive defaults to false; use RegExp matching?
- GetItemsArrayWithAllProperties(BagName, ItemPropertyNameArray) = array of all items which have all of the properties in ItemPropertyNameArray (per the ArrayHasAllItemProperties function)
- GetItemsArrayWithoutProperties(BagName, ItemPropertyName/Array) = an array of ItemNames in BagName that do not have any ItemPropertyName/Array as any of their properties
- GetItemsDefaultType(BagName, ItemName) Returns item's default item type if it has one, "-" if it doesn't, or undefined on error.
- GetItemWherePropertyEquals(BagName, ItemPropertyName, Value) = get a random ItemName where property == Value, if ItemPropertyName points to an array, see if Value is in that array, returns "" if none found
- GetItemWherePropertyGreaterThan(BagName, ItemPropertyName, Amount) = random ItemName where property value > Amount
- GetItemWherePropertyLessThan(BagName, ItemPropertyName, Amount) = random ItemName where property value < Amount
- GetItemWithHighestPropertyValue(BagName, ItemPropertyName) = ItemName  (items without ItemPropertyName are ignored)
- GetItemWithLowestPropertyValue(BagName, ItemPropertyName) = ItemName  (items without ItemPropertyName are ignored)
- GetMatchingItemsArray(BagName, ItemName, SearchBag, [PropertyExceptionArray]) = array of all items in SearchBag that match ItemName (per the ItemsMatch function), not including excluded property names
- GetObjectOfItemPropertyValues(BagName, ItemPropertyName) return { ItemName : ItemPropertyName's value, ... }
- GetRandomItem(BagName) = random ItemName in BagName
- GetRandomItemPropertyValue(BagName, ItemPropertyName) = value of the ItemProperty for a random item in BagName
- GetRandomItemValue(BagName, ItemPropertyName) = a random value of an item's ItemPropertyName in a bag if it has that property.  Sets that item as the current item
- GetTotalItemPropertyValue(BagName, ItemPropertyName) = total of all items' ItemPropertyName values (multiplied by UInvQuantity) added together (all values must be numbers; items without ItemPropertyName are treated as having a value of zero)
+ GetUniqueItemName() returns a name not used by any item currently ("item0ffd39" 0-255 in hex * 3)
- GetUniqueItemPropertyValuesArray(BagName/Array, ItemPropertyName) returns an array of unique values for all items with ItemPropertyName in all bags in BagName/Array (use GetUniqueItemTagsArray instead for properties which have array values)
+ ItemExists(ItemName) = t/f, checks GetDefaultItemObject function and all bags for ItemName
- ItemHasAllProperties(BagName, ItemName, ItemPropertyNameArray) = t/f if all of the item's properties are listed in ItemPropertyNameArray
- ItemHasAnyProperties(BagName, ItemName, ItemPropertyNameArray) = t/f if any of the item's properties are listed in ItemPropertyNameArray
+ ItemHasProperty(BagName, ItemName, ItemPropertyName) = t/f
- ItemPropertyHasValue(BagName, ItemName, ItemPropertyName, Value) = t/f if item's property ==/contains Value
+ ItemsMatch(BagName1, ItemName1, BagName2, ItemName2, [PropertyExceptionArray]) = t/f if all properties exist and match, except quantity, UInvDefaultItemType, and UInvCell, plus any properties in PropertyExceptionArray if included
+ MoveItem(SourceBagName, DestinationBagName, ItemName, [Quantity], [NewItemName])  if no Quantity parameter then move all
MoveItems(SourceBagName, DestinationBagName, ItemNameArray, [Quantity]) moves all items in the item array (or Quantity of each item) from source to destination, skipping any items that don't exist in the source, returns number of item types moved(?)
- MoveBagPropertyValueToItem(SourceBagName, SourceBagPropertyName, DestinationBagName, DestinationItemName, [DestinationItemPropertyName], [Amount], [MinimumValue], [MaximumValue], [DeletionValue], [DeletionType])
- MoveItemPropertyValueToItem(SourceBagName, SourceItemName, SourceItemPropertyName, DestinationBagName, DestinationItemName, [DestinationItemPropertyName], [Amount], [MinimumValue], [MaximumValue], [DeletionValue], [DeletionType])
- MoveItemsByProperty(SourceBagName, DestinationBagName, ItemProperty, [Value]) move all items in bag that have the ItemProperty, or ItemProperty === Value if Value is passed
+ RenameItem(BagName, CurrentItemName, NewItemName, [Quantity])  Quantity for partial stacks of items, copy in default item properties
- RenameItemProperty(BagName, ItemName, CurrentItemPropertyName, NewItemPropertyName) Renames item property.  Returns true if it succeeds.  Bag is not touched.
- ResetItemProperties(BagName/Array, ItemName, [DefaultItemName]) removes all properties then loads default
+ RestackItems(BagName) Attempts to restack any items that may be stacked incorrectly.
+ SetCurrentItemName(ItemName)
+ SetItemPropertyValue(BagName, ItemName, ItemPropertyName, Value) add property if it doesn't exist
- SetItemPropertyValues(BagName, ItemName, ValuesObject) Updates and/or adds values to multiple properties on an item
- SetItemQuantity(BagName, ItemName, Quantity)  Quantity must be a positive integer
- SetItemsDefaultType(BagName, ItemName, DefaultItemType)
- SetItemsPropertyValue(BagName, ItemPropertyName, Value) sets the value of ItemPropertyName on all items in a bag
+ SwapItems(BagName1, ItemName1, BagName2, ItemName2, [ExceptItemPropertyName/array]) swaps two items, optionally keeps certain item properties un-swapped
- SwapItemsProperties(BagName1, ItemName1, BagName2, ItemName2, ItemPropertyName/array) Swaps the given properties of two items.  Returns true on success, false on error.

"Bind" functions, so that changing one property value on one item, changes another value on another item?
Have the "Bind" functions call a function/widget and tell it what property changed, what it changed from, and what it changed to
Do not trigger a "Bind" call if the value was "set" but not changed. (So they don't loop back and forth.)
(option to "copy-and-link" items, so that any property changes to one will happen to all linked items, so that all stay identical?)

(make more ...CountFull... functions?  Or add a parameter to existing functions instead?)
(add the ability to test by date?  e.g. is property X > someDate + days + hours + minutes?)

2 more to complete!  (85 functions)



Tag functions:
--------------

- AddBagTag(BagName/Array, BagPropertyName, BagTag/Array)
- AddItemTag(BagName/Array, ItemName, ItemPropertyName, ItemTag/Array)
- ArrayHasAllItemTags(BagName, ItemName, ItemPropertyName, ItemTagArray) = t/f if all of the item's tags exist in ItemTagArray
- BagHasAllBagTags(BagName, BagPropertyName, BagTagArray) = t/f
- BagHasAllItemTags(BagName, ItemPropertyName, ItemTagArray) = t/f
- BagHasAnyBagTag(BagName, BagPropertyName, BagTagArray) = t/f
- BagHasAnyItemTag(BagName, ItemPropertyName, ItemTagArray) = t/f
- BagHasItemByBagTag(BagPropertyName, BagTag, ItemName) = true if any bags with BagPropertyName/BagTag have item
- BagHasAllItemsByBagTag(BagPropertyName, BagTag, ItemNameArray) = t/f if any bag with BagPropertyName/BagTag has all of the items in ItemNameArray
- BagHasAnyItemByBagTag(BagPropertyName, BagTag, ItemNameArray) = t/f if any bag with BagPropertyName/BagTag has any of the items in ItemNameArray
A BagHasItemByItemTag(BagName, ItemPropertyName, ItemTag) = t/f
- BagHasItemWithAllItemTags(BagName, ItemPropertyName, ItemTag/Array) = t/f
- BagHasItemWithAnyItemTag(BagName, ItemPropertyName, ItemTag/Array) = t/f
- BagHasItemWithoutAllItemTags(BagName, ItemPropertyName, ItemTag/Array) = t/f
- BagHasItemWithoutAnyItemTags(BagName, ItemPropertyName, ItemTag/Array) = t/f
- BagHasTag(BagName, BagPropertyName, BagTag) = t/f
A CopyItemsByItemTag(SourceBagName, DestinationBagName, ItemPropertyName, ItemTag) copy all items in bag that have the ItemTag
- DeleteBagTag(BagName/Array, BagPropertyName, BagTag/Array)
A DeleteItemsByItemTag(BagName, ItemPropertyName, ItemTag) delete all items in bag that have the ItemTag
- DeleteItemTag(BagName/Array, ItemName, ItemPropertyName, ItemTag/Array)
- GetAllUniqueItemTagsArray(BagName, ItemPropertyName) = array of all unique item tags in the ItemPropertName property for all items in bag
- GetBagsArrayByBagTag(BagPropertyName, BagTag) = array of bag names that have BagTag in BagPropertyName
- GetBagsArrayWithBothBagTags(BagPropertyName1, BagTag1, BagPropertyName2, BagTag2) = array of bag names
- GetBagsArrayWithItemByBagTag(BagPropertyName, BagTag, ItemName) = array of bag names from bags with tag and item
- GetBagTagQuantityObject(BagName, BagPropertyName) = { "UniqueBagTag1" : QuantityOfBagTag1, ... }
- GetFullItemCountByAllItemTags(BagName, ItemPropertyName, TagArray) = full number of items with all tags in that property
- GetFullItemCountByAnyItemTag(BagName, ItemPropertyName, TagArray) = full number of items with any of those tags in that property
- GetItemsArrayByAllItemTags(BagName, ItemPropertyName, ItemTagArray) = array of item names with all tags
- GetItemsArrayByAnyItemTag(BagName, ItemPropertyName, ItemTagArray) = array of item names with any tags
A GetItemsArrayByItemTag(BagName, ItemPropertyName, ItemTag) = array of item names that have tag
- GetItemsArrayWithAllItemTags(BagName, ItemPropertyName, ItemTagArray) = array of all items which have all of their tags in ItemTagArray (per the ArrayHasAllItemTags function)
A GetItemsArrayWithBothItemTags(BagName, ItemPropertyName1, ItemTag1, ItemPropertyName2, ItemTag2) = array of item names
- GetItemsArrayWithMostItemTags(BagName, ItemPropertyName, ItemTagArray) = array of all items that are tied for the most ItemTags in ItemPropertyName
- GetItemsArrayWithoutAllItemTags(BagName, ItemPropertyName, ItemTagArray) = array of item names without all of these tags
- GetItemsArrayWithoutAnyItemTags(BagName, ItemPropertyName, ItemTagArray) = array of item names without any of these tags
A GetItemTagCount(BagName, ItemName, ItemPropertyName, Tag) = the number of times Tag is found in that tag array
- GetItemTagQuantityObject(BagName, ItemName, ItemPropertyName) = { "UniqueItemTag1" : QuantityOfItemTag1, ... }
- GetMissingBagTagsArray(BagName, BagPropertyName, BagTagArray) returns an array of all tags in BagTagArray which were not found on the BagPropertyName for that bag
- GetMissingItemTagsArray(BagName, ItemName, ItemPropertyName, ItemTagArray) returns an array of all tags in ItemTagArray which were not found on the items' ItemPropertyName in that bag
- GetRandomBagTagFromRange(BagName, BagPropertyName, HighIndex, [LowIndex = 0]) Returns a random tag from LowIndex to HighIndex (inclusive)
- GetRandomItemTagFromRange(BagName, ItemName, ItemPropertyName, HighIndex, [LowIndex = 0]) Returns a random tag from LowIndex to HighIndex (inclusive)
- GetUniqueBagTagsArray(BagPropertyName, [BagName/Array]) = [ "UniqueTag1", ... ]
- GetUniqueItemTagsArray(ItemPropertyName, [BagName/Array]) = [ "UniqueTag1", ... ]
- ItemHasAllTags(BagName, ItemName, ItemPropertyName, ItemTagArray) = t/f
- ItemHasAnyTag(BagName, ItemName, ItemPropertyName, ItemTagArray) = t/f
A ItemHasTag(BagName, ItemName, ItemPropertyName, ItemTag) = t/f
A MoveItemsByItemTag(SourceBagName, DestinationBagName, ItemPropertyName, ItemTag) move all items in bag that have the ItemTag
- SetBagTag(BagName/Array, BagPropertyName, BagTag/Array, Enabled) adds or removes all tags based on Enabled
- SetItemTag(BagName/Array, ItemName, ItemPropertyName, ItemTag/Array, Enabled) adds or removes all tags based on Enabled

*** Have any "sort" functions sort numbers first, then strings

(add options to shuffle around or sort tags)

8(?) functions to modify  (49 functions)



Display functions and elements:
-------------------------------

- AddEventHandler(Group, Event, Handler, [Options]) Adds an event handler.  If that exact handler already exists it returns the existing HandleID, otherwise returns a new HandleID on success, or "false" on error.
- CallEventHandler(Group, Event, Values) The same as CallEventHandlerEx, but all handler returns are combined into a single generic object.
- CallEventHandlerEx(Group, Event, Values) Triggers any matching event handlers and passes the Values object to them.  Returns an array of results from all triggered handlers (results will either be generic objects or undefined), returns "false" on error.
- DecrementUpdateLock() Reduces the LockCount by 1
- DeleteEventHandler(Group, Event, Handler/HandlerID, [Options]) Deletes any matching event handlers.  Returns the number of deleted handlers, or false on error.
- DisplayArray(YourArray, [EmptyString], [SeparatorString], [ConjunctionString], [UseAPNumbers])
* DisplayItemList(BagName, [PluralItemPropertyName], [EmptyString], [SeparatorString], [ConjunctionString], [SingleItemPropertyName])
- DisplayRadialMenu(WedgeItems, Position, Handler, Options)
- FixTableCells(BagName, UInvTable) Makes sure each item is assigned a unique cell.
GetElementVisibility(element) = % visible (use Intersection Observer API https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API )
- GetEventHandlerByID(Group, Event, HandlerID) Returns a UInvEventHandler object if one exists, or "null" if none exist, or "undefined" on error.
- GetMatchingEventHandlersArray(Group, Event, [Options], [Handler]) Returns an array of the handler ID of all handlers that match the parameters passed in (Handler and/or Options are optional), or "false" on error.
+ GetUpdateLocks() = the number of locks on updates.  Does not prevent manual updates via. UInv.UpdateDisplay().
- IncrementUpdateLock() Increases the LockCount by 1
- UpdateDisplay([Container]) Updates the display of any UInv HTML elements, such as tables.
- UpdatesAreLocked() Returns whether automatic display updates are locked or not.
<<UInvDisplay>> (see below)
Clothing Mannequin
Dropdown List
Health Bars/Status Indicator  (show icon and bar indicating various stats for things like health and mana bars)
Item Property Slider (BagName, Item, ItemPropertyName, MinVal, MaxVal, [Increment])
* Radial Menu
* Table (see "Tables:" notes below)
Toolbar  (show a toolbar for triggering various things, such as showing the inventory, etc...)
Image rotation and mirroring?
etc...

9? items remaining.  (16 functions + 8 display elements; 15 functions written + 2 display elements)  (total will probably be 16-20 functions + 1 macro + 7 display elements)

<<UInvDisplay>>
Inventory:
^For:BagName:"backpack"
	^Prop:"UInvQuantity" - ^Prop:"name"   ^Link:Drop 1:DeleteItem(^Bag, ^Item, 1):ItemHasTag(^Bag, ^Item, "type", "undroppable"):
^/For
<</UInvDisplay>>

The above would loop through backpack, showing the quantity and name for each item, plus a link saying "Drop 1" which would delete 1 of that item as long as it didn't have "undroppable" in the array in its "type" property.
(Make it easy to do in table format.  Make adding images easy.)

Display bugs:
 • Radial menu in wrong location in IE 11.
 • Table displayed slightly wrong in iPhone and iPad devices.
 • On Android/Chrome the table is displayed behind UI bar and items can't be dragged.
 • Laptops with touchscreen won't drag items in Firefox (but it works in Edge).
 • Look into preventing console error spam.

(Tables: Make searchable (text search), sortable, filter by type, grouped on tabs by type, max-stacks/weight/slots? (individual or global max-stack; for max-weight show a bar which indicates how "full" the bag is))
(Tables: Make single or multiple items selectable.  Make tables able to gain/lose focus.  Event handling for keypresses based on table focus and selected items, so the developer can, for example, allow multiple items to be deleted using a keypress.)

(add a function to "equip/unequip"/"pickup/drop" items with text links)

(make a way to easily represent comparisons between items of the same type, e.g. show which weapon stats are better and/or worse than the currently equipped weapon)

(add item "health bars" for table)

(add a "shop" display; limited and unlimited stores)

(add a "clothing mannequin" for dressing and equipping characters: modal window overlay, left-hand box shows character info, property names are plain text with their values in boxes; right-hand box shows the "clothing mannequin" but with an arrow button that can be pressed/toggled to swap to a text description)

(have a way to add additional tabs to the sidebar to show various content (inventory, stats, skills, settings, etc..) without changing the active passage)

(If you move an item to a filled slot, have an option to swap items or destroy the destination item (with warning).)

(During testing: Make sure that the UpdateDisplay function is only called once when performing any change.)

$UInvEventHandlers groups and events:

All events:  (the below properties are included in all other events)
-----------
   + Event
          .eventGroup = this event's Group type
          .eventType = this event's Event type
          .handlerID = HandlerID which is triggering this event


"general" events:
-----------------

 - MouseDown (sends MouseDown event)
   > See: https://developer.mozilla.org/en-US/docs/Web/Events/mousedown
          https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent
   + Event
          (no additional properties besides .eventGroup, .eventType, and .handlerID)
   + Return
            = { stopPropagation: true } (prevents event from further propagating)
              > See: https://developer.mozilla.org/en-US/docs/Web/API/Event/stopPropagation

 - MouseUp (sends MouseUp event)
   > See: https://developer.mozilla.org/en-US/docs/Web/Events/mouseup
          https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent
   + Event
          (no additional properties besides .eventGroup, .eventType, and .handlerID)
   + Return
            = { stopPropagation: true } (prevents event from further propagating)
              > See: https://developer.mozilla.org/en-US/docs/Web/API/Event/stopPropagation


"bag" events:
-------------

 - Touched (sends generic object)  [handle multi-touch Start and Stop events?]
   + Event
          .bagName = name of bag which is being marked as touched
          .lockCount = the current display update "lock count"
   + Return
            = { ignoreTouch: true } (prevents bag from being marked as Touched)



"table" events:
---------------

 - Accept (sends element to be dropped)
   NOTE: The table Accept event gets called many times during drag-and-drop actions.
         It gets called once for each droppable element after DragStart and before DragStop,
         plus once on each DragOver and DragOut event.
         Any event handlers for this event should finish as quickly as possible.
   + Event
          .srcBag = name of the bag the item was dragged from
          .draggedItem = name of the dragged item
          .destBag = name of the bag it's being dropped into
          .droppedOnItem = if it's being dropped on another item it's the item's name, otherwise it's "" (an empty string)
          .newCellNo = the UInvCell number in destBag that item is being dropped on
          .acceptVal = the current choice of whether to accept or reject the drop
   + Return
            = { acceptVal: true/false }   (prevents allows/prevents drop action)

 - DragStart (sends DragStart event)
   + Event
          .srcBag = name of the bag the item was dragged from
          .draggedItem = name of the dragged item
          .ui = the DragStart event's UI object
   + Return (none)

 - DragStop (sends DragStop event)
   + Event
          .srcBag = name of the bag the item was dragged from
          .draggedItem = name of the dragged item
          .ui = the DragStop event's UI object
   + Return (none)

 - Drop (sends Drop event)
   + Event
          .srcBag = name of the bag the item was dragged from
          .draggedItem = name of the dragged item
          .oldCellNo = the UInvCell number in srcBag that the item was dragged from
          .destBag = name of the bag it's being dropped into
          .droppedOnItem = if it's being dropped on another item it's the item's name, otherwise it's "" (an empty string)
          .newCellNo = the UInvCell number in destBag that item is being dropped on
          .pos = { x: mouseLeft, y: mouseTop }
          .ui = the Drop event's UI object
   + Return
            = { overrideDefaultAction: true }                (prevents default drop action)
            = { openRadialMenu: true }                       (opens the radial menu, overrides the default drop action; requires .radialMenuWedgeItems and .radialMenuHandler)
            = { radialMenuWedgeItems: UInvRadialWedgeItems } (see DisplayRadialMenu)
            = { radialMenuHandler: Handler }                 (see DisplayRadialMenu)
            = { radialMenuOptions: UInvRadialOptions }       (see DisplayRadialMenu)

ToDo: add ItemClick and ItemDoubleClick events


"radialMenu" events:
--------------------

 - Open (sends radial menu div element)
   + Event
          .radialMenuWedgeItems = WedgeItems;
          .radialMenuHandler = Handler;
          .radialMenuOptions = Options;
   + Return
            = { radialMenuWedgeItems: UInvRadialWedgeItems } (see DisplayRadialMenu)
            = { radialMenuHandler: Handler }                 (see DisplayRadialMenu)
            = { radialMenuOptions: UInvRadialOptions }       (see DisplayRadialMenu)

 - WedgeClick (sends MouseUp event)
   + Event
          .wedgeID = The wedge's ID number (0 = center, 1 = top, 2... rotate clockwise)
          .wedgeData = The wedge's data string
          .wedgeDisabled = false
          .srcBag = name of the bag the item was dragged from
          .draggedItem = name of the dragged item
          .oldCellNo = the UInvCell number in srcBag that the item was dragged from
          .destBag = name of the bag it's being dropped into
          .droppedOnItem = if it's being dropped on another item it's the item's name, otherwise it's "" (an empty string)
          .newCellNo = the UInvCell number in destBag that item is being dropped on
          .radialMenuWedgeItems = current radialMenuWedgeItems values
          .pos = { x: radial menu center X position, y: radial menu center y position }
          .radialMenuHandler = current radialMenuHandler value
          .radialMenuOptions = current radialMenuOptions values
   + Return
            = { keepOpen: true }                             (don't close radial menu)
            = { overrideDefaultAction: true }                (prevents default click action)
            = { radialMenuWedgeItems: UInvRadialWedgeItems } (see DisplayRadialMenu)
            = { radialMenuHandler: Handler }                 (see DisplayRadialMenu)
            = { radialMenuOptions: UInvRadialOptions }       (see DisplayRadialMenu)

 - DisabledWedgeClick (sends MouseUp event)
   + Event
          .wedgeID = The wedge's ID number (0 = center, 1 = top, 2... rotate clockwise)
          .wedgeData = The wedge's data string
          .wedgeDisabled = true
          .srcBag = name of the bag the item was dragged from
          .draggedItem = name of the dragged item
          .oldCellNo = the UInvCell number in srcBag that the item was dragged from
          .destBag = name of the bag it's being dropped into
          .droppedOnItem = if it's being dropped on another item it's the item's name, otherwise it's "" (an empty string)
          .newCellNo = the UInvCell number in destBag that item is being dropped on
   + Return
            = { keepOpen: false } (close radial menu)
            = { radialMenuWedgeItems: UInvRadialWedgeItems } (see DisplayRadialMenu)
            = { radialMenuHandler: Handler }                 (see DisplayRadialMenu)
            = { radialMenuOptions: UInvRadialOptions }       (see DisplayRadialMenu)

 - Cancel (sends :passagerender event for "NewPassage", otherwise sends MouseDown/Up event)
   + Event
          .cancelType = "NewPassage", "MouseDown", or "MouseUp"
   > See: http://www.motoslave.net/sugarcube/2/docs/#navigation-event-passagerender
          http://www.motoslave.net/sugarcube/2/docs/#passage-api
          https://developer.mozilla.org/en-US/docs/Web/Events/mousedown
          https://developer.mozilla.org/en-US/docs/Web/Events/mouseup
          https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent
   + Return
            = { keepOpen: true } (don't close radial menu)


"cacheImages" events:
---------------------

 - Loaded (triggered on "load" events)
   > See: https://developer.mozilla.org/en-US/docs/Web/Events/load
          https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/onload
   + Event
          .path       = image's path
          .filename   = image's filename
          .URL        = image's URL (= .path + .filename)
          .tries      = the number of times the image has tried to load
          .src        = image's source URL
          .imageGroup = (optional) if the handler was set using UInv.cacheImages, this is a unique group ID
   + Return (none)

 - Error (triggered on "error" and "abort" events)
   > See: https://developer.mozilla.org/en-US/docs/Web/Events/error
          https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/onerror
          https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/onabort
   + Event
          .path       = image's path
          .filename   = image's filename
          .URL        = image's URL (= .path + .filename)
          .tries      = the number of times the image has tried to load
          .src        = image's source URL
          .imageGroup = (optional) if the handler was set using UInv.cacheImages, this is a unique group ID
   + Return
            = { retryLoad: true } (retry loading this image)

 - Idle (triggered after the last image in the cache has been processed)
   + Event
          .complete = The total number of images that have either successfully loaded or failed to load.
          .loaded   = The total number of images that have successfully loaded.
          .errors   = The total number of images that have failed to load.
   + Return (none)



"visibility" events:
--------------------

Use Intersection Observer API and Page Visibility API to trigger events based on changing visibility of elements.
https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API


Radial menus:
https://codepen.io/thomasbaldry/pen/RLaxoP
https://codepen.io/lenymo/pen/rwmBGq
https://codepen.io/web-tiki/pen/ZYmZoV
https://codepen.io/Fernker/pen/Ikjdq

fa-sync-alt = swap items
fa-plus-square = drop all items
plus-half_white.svg = drop half of items (round down)
plus-quarter_white.svg = drop a quarter of items (round down)
plus-one_white.svg = drop one item
minus-one_white.svg = pick up one item
fa-times-circle = cancel radial menu


Visual Inventory Designer (VID) sample code:
https://codepen.io/Prebyter/pen/ggVxWo
https://codepen.io/cjonasw/pen/Jiqdj
https://codepen.io/101Computing/pen/vdPoMw  (Minecraft style)
https://codepen.io/MadLittleMods/pen/vmhLF  (Inventory System - Drag and Drop Resizable Grid)
https://codepen.io/TLJens/pen/RPWBvY  (inset boxes)
https://css-tricks.com/creating-a-parking-game-with-the-html-drag-and-drop-api/
https://www.w3schools.com/html/html5_draganddrop.asp
https://www.w3schools.com/html/tryit.asp?filename=tryhtml5_draganddrop2
https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API
https://twinery.org/questions/10028/how-to-have-a-drag-and-drop-div-list-its-contents
https://twinery.org/questions/1021/using-drag-and-drop-to-set-variables
https://developer.mozilla.org/en-US/docs/Web/CSS/border-image
http://designwithpc.com/Plugins/ddSlick
https://stackoverflow.com/questions/623172/how-to-get-image-size-height-width-using-javascript
https://stackoverflow.com/questions/118241/calculate-text-width-with-javascript
https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file

(Allow them to be in translucent boxes over the text?  Collapseable inventory?)
(Allow user to store "themes" in designer.)
(Allow drag-drop blacklists and/or whitelists + function checks(?))
(Browser pre-caching of images?)

Class stuff:
http://api.jquery.com/addClass/
https://stackoverflow.com/questions/16086201/jquery-changing-style-of-html-element
https://stackoverflow.com/questions/11474430/change-css-class-properties-with-jquery

https://api.jqueryui.com/draggable/
https://api.jqueryui.com/droppable/



Other UInv functions and macros:
--------------------------------

+ ClearErrors() Clears error message.
+ GetLastError([Clear]) = last error message, also clears the error messages if Clear is set to true
+ GetUserAlerts()
+ Initialize([DisplayErrors]) create and set global variables
+ SetUserAlerts(ErrorSetting, ErrorStringAddendum) Enables/disables display of error message dialog boxes with Text addendum.  (This can be used for debugging and/or to let users know how to report this error.)
+ SetMergeItemMethod(MergeMethod) call this to determine how items will be merged if source and destination items do not match (other than UInvQuantity)
+ Version() = version info
- <<UInvSet>><</UInvSet>>
- <<UInvTry CodeString>><<Fail>><</UInvTry>>

Complete!!!  (9 functions/macros)

(Store error messages in a separate file to allow translations?)
(Add cheat detection?  Enable/disable with password, store hash of password.)


Utility functions:
------------------

+ addArticle(WordIn, Caps)  Returns "a " or "an " followed by the string passed in, based on the first word passed to it.
- arrayObjectIncludes(ArrayOfGenericObjects, ObjProperty, Value) Returns whether the array has any objects with the property ObjProperty that has a matching value.
+ arraysAreEqual(Array1, Array2) = t/f
- arrayHasAllTags(Array, TagArray) = t/f  (if a tag in TagArray is repeated, Array must have that many or more of that tag in it to count)
- arrayHasAnyTag(Array, TagArray) = t/f  (if a tag in TagArray is repeated, Array must have that many or more of that tag in it to count)
+ arrayHasTag(Array, Tag) = number of Tag tags found in array, or undefined on error
+ cacheImages(Path, ImageName/array, [Handler]) Allows you to preload images.  You can use the handler to receive notifications about load or error events.
- combineGenericObjects(Obj1, Obj2) Returns a new object that has the combined properties of Obj1 and Obj2, with Obj2's properties preferred when both objects have matching property names.  Returns "undefined" on error.
- docHasCSSElement(CSSElement) If document has CSS element "CSSElement", returns the element's CSSStyleDeclaration object, otherwise returns "false".
+ flushAllCachedImages([MaxCache, MaxConcurrent]) Clears out all cached images.  Also lets you set the maximum number of images to cache (defaults to 100) and the maximum number of images do download concurrently (defaults to 5).  Returns true on success and false on error.
- flushCachedImages(Path, ImageName/array) Allows you to manually unload previously cached images.
- getArraySortedByOtherArray(UnsortedArray, ArrayToSortBy, [RemoveDuplicates = false]) = UnsortedArray sorted based on ArrayToSortBy and subsorted by UnsortedArray value, if RemoveDuplicates is true, remove any elements with the same values across both arrays
- getArrayReverseSortedByOtherArray(UnsortedArray, ArrayToSortBy, [RemoveDuplicates = false]) = UnsortedArray reverse sorted based on ArrayToSortBy and subsorted by UnsortedArray value, if RemoveDuplicates is true, remove any elements with the same values across both arrays
- getCachedImageObject(Path, ImageName) Returns a copy of the cached image object.  This way you can access properties like .naturalWidth and .naturalHeight on it.  Returns "null" if image not in cache or "undefined" on error.
+ getObjectProperties(Object, [Ext]) Returns all of the properties and values of an object as a string.  Non-objects get returned unchanged.
+ getRandomHexString() Returns a random hexidecimal string of 6 characters.
- getUniqueArray(Array) = an array so that all elements of the original array are now unique, or undefined on error.
- integerToOrdinal(Value, [Options]) = an ordinal string (e.g. "first", "11th", "1,000th", etc...), or same value if Value isn't an integer. Options = "NoOrdinalText", "UseSuperscript", "Capitalize"
+ isArray(Value) = t/f
+ isArrayOfArrays(Value) = t/f
+ isArrayOfBooleans(Value) = t/f
- isArrayOfDates(Value) = t/f
+ isArrayOfGenericObjects(Value) = t/f
+ isArrayOfIntegers(Value) = t/f
- isArrayOfMaps(Value) = t/f
+ isArrayOfNumbers(Value) = t/f
+ isArrayOfObjects(Value) = t/f
- isArrayOfSets(Value) = t/f
+ isArrayOfStrings(Value) = t/f
- isArrayOfType(Value, Type) = t/f
+ isBoolean(Value) = t/f
+ isDate(Value) = t/f
+ isFunction(Value) = t/f
+ isGenericObject(Value) = t/f
+ isInteger(Value) = t/f
+ isMap(Value) = t/f
+ isNumber(Value) = t/f
+ isObject(Value) = t/f
+ isProperty(Obj, Prop) = Returns if Prop is a property of the object Obj.
+ isRegExp(Value) = t/f
+ isSet(Value) = t/f
+ isString(Value) = t/f
+ isUndefined(Value) = t/f
+ mapsAreEqual(Map1, Map2) =  t/f
♦ numberToAPString(Value) = returns the number as an AP style formatted string
+ objectsAreEqual(Object1, Object2) = t/f
sanitizeString(String) = String minus any code stuff ("$", "_", "@@", "&#", "[[", "[", "<<", "<", + URLDecoded versions of same (repeat URLDecode until string stops changing; also add ";" for any that start with "&#"(?).))
+ setsAreEqual(Set1, Set2) =  t/f
+ setsMatch(Set1, Set2) = if two sets contain matches for all values in any order.
+ spread(Value, Funct) = Returns a Map, Set, or String converted to an array.  If the second parameter is an Array, Map, Set, or String, then the two objects are spread and returned as a single array.  If a function is passed as the second parameter, this calls the function with the spread array as parameters and returns that functions value.
+ valuesAreEqual(Var1, Var2) = t/f
+ isAndroid
+ isBlackBerry
+ isBlink
+ isChrome
+ isEdge
+ isFirefox
+ isIE
+ isiOS
+ isMobile
+ isOpera
+ isSafari
+ isTwine

more array functions?  (sort, reverse array order, make unique, merge arrays, shuffle, etc. ...)

documentation To-Do list: isMap, isSet, mapsAreEqual, setsAreEqual, setsMatch, spread

1 more to complete!  (63 functions)



295+ functions/macros + 8+ display elements planned
291  functions/macros + 2  display elements written
= ~97% complete



Internal-use-only functions:
----------------------------

Available:
+ continueLoadingCache() - Starts loading any waiting images if maxConcurrent images aren't already loading.
♦ GetRawBagObject(BagName) - For testing purposes only: Returns the raw bag object.
♦ GetRawItemObject(BagName, ItemName) - For testing purposes only: Returns the raw item object.
+ initializeImageCache() - Sets up setup.UInvImageCache for image caching.
+ InitializeRadialMenu() - Creates radial menu div element.

Hidden:
♦ deepFreeze(Object) - Freeze everything in an object's property tree.
♦ FixBagName(BagName) - If BagName is an empty string it returns the current bag name (if there is one), otherwise return BagName, unless BagName isn't a string, in which case return false.
- FixContainerReferences(OldBagName, OldItemName, NewBagName, NewItemName) - Fixes pockets on new item so that they refer to the item's new bag and/or item name(s).
♦ FixItemName(ItemName) - If ItemName is an empty string it returns the current item name (if there is one), otherwise return ItemName, unless ItemName isn't a string, in which case return false.
+ RemoveItemObjectsDefaultProperties(Obj, DefaultItemType) - Removes all default properties from Obj.  Returns true on success or false on error.
+ TryIntParse(Value) - Attempts to parse strings to integers if Value is a string, returns either a number or undefined if Value isn't a number.
+ UInvError(ErrStr) - Triggers a UInv error message.
- ValidateItemName(ItemName) - Returns validated ItemName or undefined on failure.

(13 internal functions - not counted in total above)


User Notes:
-----------

BagData function stores the list of default bags.
ItemData function stores the list of default items.

*** vv In the ItemData function vv ***

Item names must be unique strings in all lowercase.
Item names cannot be "UInvTouched", "UInvProperties", "UInvDefaultBagType", "UInvContainer", or "".
Property names can be upper and/or lowercase strings.
An item cannot have more than one property with the same name.
An item cannot have a property named "UInvQuantity", "UInvDefaultItemType", "UInvVariableType". "UInvPocket", or "UInvCell".
Property values can be strings, numbers, booleans, arrays, or generic objects.
Functions are unsupported property value types.
Multiple successive case lines with no "break;" between them will be treated as different items with the same properties.

*** ^^ In the ItemData function ^^ ***

New items should be added in the following format:

case "unique-lowercase-item-name-string":
	Item = { unique-property-name1-string : property-value1,
		 unique-property-name2-string : property-value2, 
		 ... };
	break;

It's recommended that you list your items alphabetically both to help avoid duplicating names and to make them easier to find if you need to modify them later.

Items in the ItemData function with multiple successive "case" lines with no "break;" between them will be treated as different items with all the same properties.  You can use this if you want to have a bunch of items that are all identical except for their names.

BagName must be unique across all bags.

BagPropertyName must be unique within any particular bag.

ItemName must be unique both within a bag and in the ItemData function.  ItemName also cannot be "UInvProperties" or "UInvTouched".

ItemPropertyNames must be unique within any item and cannot be "UInvQuantity".

UInvQuantity exists on all items and must be a positive integer.  If <= 0 then the item gets deleted from the bag.

Modified properties of items are kept between saves, but unmodified properties will be pulled from the GetDefaultItemObject function.  This means that if you change an item property in different versions of your code, players will only see that property change if the item was using the default item properties.  Use ResetItemProperties to get the default properties.

CreateItem is not recommended, since it can cause you to have duplicate ItemNames.  For the same reason, ChangeItem should be used with caution.  If two different items in two different bags have the same ItemName, and you move the item from one bag to another, the version in the receiving bag will be used and its Quantity will be incremented.

You can set "touched" to false on a bag, and if any changes are made to the number of items in that bag, "touched" will be set to true.  You can use this if you need to determine whether a bag needs to be reset or not.  For example, if a store needs to be restocked at the end of the day or not.  Changing the bag's properties or an item's properties within that bag does NOT change the bag to touched.

Q: Why not have AddItem (or some function) automatically create the bag if it doesn't already exist?
A: Because it makes it harder for you to discover errors.  If the bag name you put in had a typo in it, then no error would be thrown because the typo bag would be created, and you would probably waste a lot of time trying to figure out where the missing item went.  That is, once you finally noticed it was missing.  Having an error thrown makes noticing bugs and debugging them much easier.

It should be noted that, because the data is stored in objects, the order that you may get values from objects after they've changed can be (though usually isn't) quite different.  Do not rely on getting the same order of output twice from an object unless it's sorted in some way.

Bags are created in $UInvBags / State.variables.UInvBags as below:

$UInvBags {
    BagName : {
        [ UInvTouched  : true/false, ]  (unless it's true)
        UInvProperties : {
            [ UInvVariableType : "", ]  (if a bag's DefaultBagType is variable)
            property object for bag ... (done in "BagPropertyName : value" format)
        },
        [ UInvDefaultBagType : "DefaultBagTypeString", ]  (only if it's based on a default type)
        [ UInvContainer      : [ { BagName : "name", ItemName : "name", PocketName : "name" }, ... ], ]  (only if it's a pocket)
        ItemName : {
            [ UInvQuantity        : positive integer, ]  (unless the quantity == 1)
            [ UInvDefaultItemType : "DefaultItemTypeString", ]  (only if it's based on a default type)
            [ UInvVariableType    : "", ]  (if an item's DefaultItemType is variable)
            [ UInvPocket          : { "PocketName" : "BagName", ... } ]  (only if it has pockets)
            [ UInvCell            : positive integer, ]  (only if it's in a table)
            property object for item ... (done in "ItemPropertyName : value" format)
        },
        next item in bag ... (ItemName format)
    },
    next bag ... (BagName format)
}

BagTouched        = UInvBags[BagName]["UInvTouched"] *
BagPropertyObject = UInvBags[BagName]["UInvProperties"] *  (unless bag has no properties)
BagPropertyValue  = UInvBags[BagName]["UInvProperties"][BagPropertyName] *  (if it's not a default value)
DefaultBagType    = UInvBags[BagName]["UInvDefaultBagType"] *  (if it exists)
ItemQuantity      = UInvBags[BagName][ItemName]["UInvQuantity"] *  (= 1 if it doesn't exist)
DefaultItemType   = UInvBags[BagName][ItemName]["UInvDefaultItemType"]  (if it exists)
ItemPropertyValue = UInvBags[BagName][ItemName][ItemPropertyName]  (if it's not a default value)

$UInvCurrentBagName      = last BagName used  *
$UInvCurrentItemName     = last ItemName used *
$UInvShowAlerts          = false or 1..15     *
$UInvLastErrorMessage    = last error message
$UInvErrorStringAddendum = text to add to the end of error messages *
$UInvMergeItemMethod     = method for handling item collision in some functions (see MERGE_... values below)
$UInvUpdatesAreLocked    = a positive integer if automatic display updates are locked *
$UInvEventHandlers       = stores various event handlers
setup.UInvImageCache = { loading: 0, complete: 0, loaded: 0, errors: 0, waiting: 0, total: 0, maxConcurrent: 5, maxCache: 100, images: [] }; *

(*: May be undefined.  Use the Get or Set functions to check or modify these.)

If a bag's DefaultBagType is variable, then "UInvVariableType" must be among its UInvProperties.

If an item's DefaultItemType is variable, then "UInvVariableType" must be among its properties.

UInv "setup" objects:
 • setup.ImagePath  (fix this? or document it?)
 • setup.UInvImageCache
 • setup.UInvRadialMenuData
 • setup.UInvUserAlertsDebug
 • setup.UInvUserAlertsBackup

Not using:
 • setup.Path
 • setup.SoundPath


UInvEventHandler object:
 • handler: a string which represents the setup function's or widget's name
 • type: "function" or "widget"; tells you whether the handler name triggers a setup function or a widget
 • options: (optional) a generic object containing properties and values which are matched with properties from the event's Event object


Values for $UInvShowAlerts, used with SetUserAlerts (values can be added together except for ERROR_NONE):

ERROR_NONE = false
  Do not display any error messages to users.
ERROR_SHOW_PASSAGE_NAME = 1
  Displays the current passage name in any error messages.
ERROR_SHOW_ALERT = 2
  Displays a modal dialog box for each error message and pauses execution.
ERROR_THROW_ERROR = 4
  Throws traditional Twine/SugarCube error messages, instead of silently returning a value which indicates that a UInv error occurred.
ERROR_TO_CONSOLE = 8
  Outputs any error messages to the console window.


Values for UInvMergeItemMethod and UInv.SetMergeItemMethod to determine how UInv handles item collision:

UInv.MERGE_USE_ONLY_DESTINATION_PROPERTIES = 1
  Ignore source properties, just increment destination's quantity. (default)
UInv.MERGE_USE_ONLY_SOURCE_PROPERTIES = 2
  Delete the destination's properties, replace with the source's properties and values, and increment the quantity.
UInv.MERGE_PREFER_DESTINATION_PROPERTIES = 3
  Keep the properties and values in the destination, add any properties and values the source had but the destination didn't, and increment the quantity.
UInv.MERGE_PREFER_SOURCE_PROPERTIES = 4
  Keep the properties and values in the source, add any properties and values the destination had but source the didn't, and increment the quantity.
UInv.MERGE_RENAME_SOURCE_ITEMNAME = 5
  Rename the source's unique identifier so that it's stored separately in the destination bag.  This causes the function to return the new item name.
UInv.MERGE_FAIL_WITH_ERROR = 6
  Fail with an error.

Functions which can cause item collision: CopyAllItemsToBag, MoveAllItemsToBag, MergeBags, AddItem, AddItems, CopyItem, MoveItem, RenameItem

NOTE: If BagName or ItemName = "" when passed to a function, then UInvCurrentBagName or UInvCurrentItemName will be used instead, respectively.



Notes to self:
--------------

*** Make sure that UInvCurrentItemName and UInvCurrentBagName only get set if the function doesn't throw an error. ***

Be careful of possible single or double quotes inside of strings you may display to make sure that they don't prematurely end another string.

Make sure that BagTouched is set properly for all functions that affect item quantities!!!

Make sure that default property values are always deleted!!!  (i.e. if a value is changed, see if it matches the default, and if it does, delete it and use the default instead.)

Create either an item editor or a "sanity checker" for the GetDefaultIemObject function's items.

Keep track of "root" functions.  (e.g. CopyItem is a "root" function for handling item merging.)
  -- CopyItem, SetItemPropertyValue

UInvCurrentBagName and UInvCurrentItemName should get destination bag and destination item names if there are source and destination parameters in the function.

Auto-compress frequently used property names by using shorter aliases?

Work on better/more consistent handling of optional parameters (e.g. the Quantity parameter in AddItem, don't throw error on "" value?).

Show sample code that displays an inventory screen without leaving the passage so that an author using UInv can display an inventory screen without having to worry about re-triggering code.  (use a Display function)

Allow "array of X" parameters whenever it makes sense.

Look at various inventory pictures on Google Image and make sure UInv can do that.

Don't use UInv.GetItemsArray in the *Has* functions when you could just be checking if the property is undefined on the object or not, e.g. (State.variables.UInvBags[BagName][ItemName] !== undefined)

Try to use .filter() to get subsets of arrays from arrays?  (compare speeds with for loops):
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter

Trap value overflows?  e.g. Number.MAX_SAFE_INTEGER + 1  (9,007,199,254,740,991 + 1)  (note: Number.MAX_SAFE_INTEGER not supported in IE)

Do more "GetObjectOf..." functions?

Minecraft-style item creation functions?
(delete A & B -> create C) { source : [ "itemA", "itemB" ], result : [ "itemC" ] }

Make a "synonym" function that pulls from a list of non-recently used synonyms.

Optimize function calling?

Support multi-threading?  (i.e. "lock" all other functions until an executing function is complete to make it thread safe)

Add a "tree shaking" application which you could feed Twee source and it would spit out a minimal version of UInv?


Function name descriptor format:

Modification:    Add / Create / Delete / Copy / Move / Merge / Rename / Empty / Set / Reset
Informational:   Get / Match / Sort / Exists
Conditionals:    Where / Has / Highest / Lowest / Are / With / Without / By / Unique
Modifiers:       Equal(s) / GreaterThan / LessThan / Contains / All / Any / None / Full
Data Categories: Bag(s) / Property(ies) / Item(s) / ItemQuantity(ies)
Data Types:      Name(s) / Value(s) / Tag(s) / Array / Object / Count
Display:         Display / Fill / List



Possibly problematic property names: ***
Names that the SugarCube parser would change:
	SCReparsedNames = ['to', 'eq', 'neq', 'is', 'isnot', 'gt', 'gte', 'lt', 'lte', 'and', 'or', 'not', 'def', 'ndef'];  // Names that the SugarCube parser would change.
Probably should prevent entirely:
	'prototype', 'constructor', '__proto__', 'await'  */
Prevent unquoted property names?:
	do|if|in|for|int|new|try|var|byte|case|char|else|enum|goto|long|null|this|true|void|with|break|catch|class|const|false|final|float|short|super|throw|while|delete|
	double|export|import|native|public|return|static|switch|throws|typeof|boolean|default|extends|finally|package|private|abstract|continue|debugger|function|volatile|
	interface|protected|transient|implements|instanceof|synchronized */
Prevent names that don't start with letters?
Use this? : https://github.com/mathiasbynens/mothereff.in/tree/master/js-properties
Check property names to see if they're functions first?


Documentation:
--------------

Ways to list items in a bag
---------------------------

The classic way:

''List of items in bag "Test":'' <<set _Items = UInv.GetItems("Test")>><<if _Items.length == 0>>(empty)<<else>><<for _i = 0; _i < _Items.length; _i++>>_Items[_i] (<<print UInv.BagHasItem("Test", _Items[_i])>>)<<if _i < _Items.length - 1>>, <</if>><</for>><</if>>

Output = <b>List of items in bag "Test":</b> belt (2), black belt (1), dagger (1)

The easy way:

''List of items in bag "Test":'' <<print UInv.DisplayItemList("Test")>>

Output = <b>List of items in bag "Test":</b> two belt, black belt, and dagger

The fancy way:

''List of items in "Test":'' <<print UInv.DisplayItemList("Test", "plural", "nothing", ",", "and", "singular")>>

Output = <b>List of items in bag "Test":</b> two belts, a black belt, and a dagger



Ways to list an item's properties
---------------------------------

The clever way:

''List of item "belt" properties and values in bag "Test":''<<set _Item = UInv.GetItem("Test", "belt")>><<for _Prop, _Val range _Item>><br>- _Prop = _Val<</for>>



---


Notes from testers:
-------------------

sissykarisub:
This looks like something I need, I am at a stand still in my 2 "games" because I am more of a writer than a coder.
I have a need for a inventory system that has a main character inventory (what they are wearing/carrying), it should also have a character inventory like a wardrobe to keep the extra clothes/items. . It should also have "store" inventories. Also it be able to say swap clothes items from wardrobe to player. The player would have "slots" say like head0, head1, chest0, chest1, waist0, where items will be placed, so that if you have a bra and shirt it goes in chest0 and chest1. I know this is all not super clear and is something I have been trying to suss on my own, but mostly just getting frustrated and stalled on my two stories. If you need a tested I am glad to give it a go.



---

Sample code:
You're trapped in a creepy old house and you have to figure out how to get out before midnight.  Make a bunch of weird ways to escape and have different ones available randomly.

---

Abbreviated code for GetHighestBagPropertyValue:

var Bags = UInv.GetBagsArrayByProperty(BagPropertyName, BagNameArray);
var HiVal = Bags.map(o => o[BagPropertyName]).reduce((a, b) => Math.max(a, b));
return Bags.filter(o => o[BagPropertyName] === HiVal).random();

