It's a complicated problem with lots of moving parts. So I'll just drop what I'm doing for a small part of it - managing the item attributes and database. For my use case, the items are stored in characters inventories like this:
$char.inv = {
gp: 5,
provision: 17,
sword: 1,
sapphire: 2,
}
That's someone who is carrying a sword, 17 provisions, 5 gold pieces and a sapphire with them. As you can see, I use a plain Object as my inventory, which means I don't store the position in the inventory. On the plus side, that's fast, SugarCube can reliably save and restore the inventory, and that's all I need. Adding position and other state to the items can be done in several ways, but unless you need it, don't bother.
The IDs you see above ("gp", "sword") and so on aren't the items - they are IDs of items inside an item database and the items themselves are instances of an "Item" class.
window.Item = function(id, name, options) {
if(!id || !name) {
throw 'The item needs a valid ID and plain-text name';
}
options = options || {};
this.id = id;
this.name = name;
// Copy options
this.long = options.long || undefined;
this.short = options.short || undefined;
this.icon = options.icon || "item_unknown";
this.invisible = options.invisible;
this.value = options.value;
this.tags = options.tags || [];
this.hasTag = (t => this.tags.includes(t));
this.hasAnyTag = (ts => this.tags.includesAny(ts));
this.hasAct = (a => !!(this.acts[a]));
this.validFor = options.validFor || (ch => true);
// Copy acts
this.acts = {};
Object.keys(options).filter(k => k.startsWith('act') && k.length > 3)
.forEach(k => this.acts[k.slice(3)] = options[k]);
// Copy anything starting with "_" (internal data)
Object.keys(options).filter(k => k.startsWith('_') && k.length > 1)
.forEach(k => this[k] = options[k]);
// If the attribute's value is a simple value, return that
// If the value is a function, call it with the character and attribute name,
// If the value is an array, pick a value at random
this.describe = ((ch, attr) => {
var result = this[attr];
if(typeof result === 'function') {
console.log(this);
return result.call(this, ch, attr);
} else if(result instanceof Array) {
return result.random();
} else {
return result;
}
});
// Similar to describe, but for acts, so always returns some String.
this.act = ((ch, act) => {
if(!this.hasAct(act)) {
return '';
}
var result = this.acts[act];
if(typeof result === 'function') {
return String(result.call(this, ch, act)) || '';
} else if(result instanceof Array) {
return String(result.random()) || '';
} else {
return String(result) || '';
}
});
// Description functions using the above
this.longDesc = (ch => this.describe(ch, 'long') || '');
// Defaults to the name with a period after it, for character summary and the like
this.shortDesc = (ch => (this.describe(ch, 'short') || this.name));
};
// Make sure "late" registering of the event just calls the method, like with jQuery.ready()
(function() {
var lastEvent = undefined;
var lastData = undefined;
jQuery.event.special[":itemdone"] = {
_default: function(event, data) {
lastEvent = event;
lastData = data;
},
add: function(handleObject) {
handleObject.handler.call(this, lastEvent, lastData);
},
};
})();
jQuery(document).trigger(":itemdone");
This is in the context of a TweeGo project, so I use jQuery's custom events to make sure I don't try to create items before the class is ready to go. If you work in Twine and have all the JavaScript in the (singular) special script passage, you don't need to bother; just defining the `window.Item` object is enough.
The Item class is mostly a holder for the item data like its description, but it can also be used to define actions ("acts") - things you can do with the item. The only one I use is the "Use" action, but the code is set up in such a way as to allow any number of them.
Describing an item is done with `<<= _item.shortDesc($character) >>`, getting the icon image of the item to display with `<<= "[img[" + _item.icon + "]]">>` and calling whatever function was set up under the "Use" action with `<<= _item.act($character, 'Use')>>`. You can check if an item can be used with `_item.hasAct("Use")`.
But we don't have items in the inventory - we have item IDs. The missing part, the glue code, is an ItemDB, defined like this:
window.ItemDB = (function() {
var _list = {};
var _none = undefined;
return {
add: (cl => {
if(cl && 'id' in cl && 'name' in cl) {
_list[cl.id] = cl;
}
}),
byId: (id => _list[id] || _none),
allValidFor: (ch => Object.values(_list).filter(cl => cl.validFor(ch))),
get all() { return Object.values(_list); }
};
})();
// Make sure "late" registering of the event just calls the method, like with jQuery.ready()
(function() {
var lastEvent = undefined;
var lastData = undefined;
jQuery.event.special[":itemdbdone"] = {
_default: function(event, data) {
lastEvent = event;
lastData = data;
},
add: function(handleObject) {
handleObject.handler.call(this, lastEvent, lastData);
},
};
})();
jQuery(document).trigger(":itemdbdone");
Again, for a simple Twine project you can skip the jQuery parts. This has just a few important methods: `ItemDB.add(...)` adds an item to the DB, `ItemDB.byId(id)` gets you an Item instance from the ID (or _none, which you can define to be some "empty spot" quasi-item if you like), and `ItemDB.all` returns an array with all items defined so far.
Now we can actually add our items:
jQuery(document).one(":itemdbdone", function() {
ItemDB.add(new Item("provision", "Provision", {
short: "Provisions to help with lost stamina [+4" + Icon.STAMINA + " when used]",
tags: ["consumable"],
actUse: function(ch) {
Character.addStamina(ch, 4);
Character.removeItem(ch, "provision");
return "Munch! (+4 Stamina)";
},
icon: "item_chest",
}));
ItemDB.add(new Item("sword", "Sword", {
short: "A simple sword",
tags: ["weapon", "sword"],
icon: "item_sword",
}));
ItemDB.add(new Item("gp", "Gold Pieces", {
short: "Gold pieces; worth their weight in gold",
icon: "item_gp",
}));
ItemDB.add(new Item("sapphire", "Sapphire", {
short: "A sapphire",
tags: ["jewel"],
icon: "item_sapphire",
}));
});
Since this is in a different JS file than the classes above, I wait for the ":itemdbdone" custom event to fire before registering my items. The "provision" one is an example of the "Use" action coded - it adds 4 Stamina points to the character using it and removes one of the items from the character's inventory.