Trying to make a looting mechanic

Twine Version: 2.9.1.0
SugarCube: 2.37.0

So, I built a fight system that rewards experience points for having completed the fight. One can level up, find gold, yadda yadda. Now I want to have item drops added.

So, I made some arrays for StoryInit:

<<set $Inventory to []>>
<<set $Inventory.Gold to 0>>
<<set $Inventory.Item01 to []>>
<<set $Inventory.Item01.Name to "Wolf Pelt">>
<<set $Inventory.Item01.Count to 0>>
<<set $Inventory.Item02 to []>>
<<set $Inventory.Item02.Name to "Wolf Fang">>
<<set $Inventory.Item02.Count to 0>>
<<set $Inventory.Item03 to []>>
<<set $Inventory.Item03.Name to "Raw Meat">>
<<set $Inventory.Item03.Count to 0>>
<<set $Inventory.Item04 to []>>
<<set $Inventory.Item04.Name to "Silver Fang">>
<<set $Inventory.Item04.Count to 0>>

<<set $Temp to []>>
<<set $Temp.Gold to 0>>
<<set $Temp.Item01 to []>>
<<set $Temp.Item01.Name to "Wolf Pelt">>
<<set $Temp.Item01.Count to 0>>
<<set $Temp.Item02 to []>>
<<set $Temp.Item02.Name to "Wolf Fang">>
<<set $Temp.Item02.Count to 0>>
<<set $Temp.Item03 to []>>
<<set $Temp.Item03.Name to "Raw Meat">>
<<set $Temp.Item03.Count to 0>>
<<set $Temp.Item04 to []>>
<<set $Temp.Item04.Name to "Silver Fang">>
<<set $Temp.Item04.Count to 0>>

This is just the first four items and some gold. The reason I have two is that $Temp is just what you gain at the end of this particular fight, whereas $Inventory is your total in your bag. At the end of the fight, there’s this:

<<set $Inventory.Item01.Count += $Temp.Item01.Count>>\
<<set $Inventory.Item02.Count += $Temp.Item02.Count>>\
<<set $Inventory.Item03.Count += $Temp.Item03.Count>>\
<<set $Inventory.Item04.Count += $Temp.Item04.Count>>\

And more items to come. Side note, if you know how to add all of the counts from one to the other, that’d be great. I’m still an amateur, especially when it comes to arrays.

So, anyway, leading out of the battle to a passage where your winnings are decided, I have this:

<<set _DropCount to random(3, 5)>>

<<set $Temp.Item01.Count to 0>>
<<set $Temp.Item02.Count to 0>>
<<set $Temp.Item03.Count to 0>>
<<set $Temp.Item04.Count to 0>>

<<for _DropCount gt 0>>
<<set _D20 to random(1, 20)>>

<<if _D20 gte 1 and _D20 lt 4>>wolf pelt - 15%
<<set $Temp.Item01.Count += 1>>
<<elseif _D20 gte 4 and _D20 lt 8>>wolf fang - 20%
<<set $Temp.Item02.Count += 1>>
<<elseif _D20 gte 8 and _D20 lt 20>>raw meat - 60%
<<set $Temp.Item03.Count += 1>>
<<elseif _D20 is 20>>silver fang - 5%
<<set $Temp.Item04.Count += 1>>
<</if>>

<<set _DropCount -= 1>>
<</for>>

I’ve done a bit of looking around, and I know I could clean that up a little bit, but I’m still an amateur, so even if it’s a bit messy, at least I know this is working as intended. This is all meant to go silently, of course. The text it’s going to print is only so that I can watch what’s going on, and when I’m done, I’ll change the link that takes me to the next screen to just a goto command so that it executes the code and moves automatically to where I want it to go, where the text that the player will see is displayed. Also, I’ve left the gold out of this, because it was working as intended.

Now, the next bit looks like this:

<<set $Drops to ["Temp.Item01", "Temp.Item02", "Temp.Item03", "Temp.Item04"]>>
<<set $Drops to $Drops.filter( (id) => State.variables[id].Count !== 0 )>>
<<set $Drops to $Drops.map( (id) => State.variables[id].Name )>>

From what I understand, this is supposed to make an array of all of the items that I may have gained (first line), filter out anything that I gained none of (second line), and map it to the name of the item (third line).

The issue is that it wouldn’t return what I want it to, even if it worked. It’s throwing error messages at me.

Error: <<set>>: bad evaluation: Cannot read properties of undefined (reading 'Count')
<<set _Drops to _Drops.filter( (id) => State.variables[id].Count !== 0 )>>

I know I haven’t told it to actually print anything yet, but if I had gained two raw meat, a fang and a pelt, then this javascript:

setup.toCommaString = function(array) {
    // 0 or 1 entries, nothing to join, just convert to string
    return array.length < 2 ? array.join('') 
        // exactly 2, join with "and"
        : array.length === 2 ? array.join(' and ')
        // join most of them with commas, use "and" for the last
        : array.slice(0, array.length-1).join(', ')
            // add the serial comma here if you want it
            + ' and ' + array[array.length-1];
}

followed by this code:

You received the following:

<<= setup.toCommaString($Drops)>>

<<set $Inventory.Item01.Count += $Temp.Item01.Count>>\
<<set $Inventory.Item02.Count += $Temp.Item02.Count>>\
<<set $Inventory.Item03.Count += $Temp.Item03.Count>>\
<<set $Inventory.Item04.Count += $Temp.Item04.Count>>\

would tell me that I gained (wolf pelt, wolf fang and raw meat), but not how many of each. I want the player to know how many of each.

That javascript is strictly reused; I haven’t made it around to fiddling with it yet, but I was going to add in a statement for if you happened to gain no items, such as a small creature that dropped 0-2 items instead of 3-5. I do want to keep that javascript, I just want to have a second, separate one, because this one is doing something else that I need it to keep doing. I just wanted to make the array work, first.

That javascript and the sorting code, I learned here, thanks to the two users that helped me figure that one out. And sorry for the wall of text.

Why are you defining an Array, which is a collection object-type for storing an ordered list of elements…

<<set $Inventory to []>>
...
<<set $Temp to []>>

…and then using that Array as if it was a Generic Object…

<<set $Inventory.Gold to 0>>
<<set $Inventory.Item01 to []>>
...
<<set $Temp.Gold to 0>>
<<set $Temp.Item01 to []>>

If you want a collection data-type that supports named properties, then you should be using a Generic Object…

<<set $Inventory to {}>>
<<set $Inventory.Gold to 0>>
<<set $Inventory.Item01 to {}>>
<<set $Inventory.Item01.Name to "Wolf Pelt">>
...
<<set $Temp to {}>>
<<set $Temp.Gold to 0>>
<<set $Temp.Item01 to {}>>
<<set $Temp.Item01.Name to "Wolf Pelt">>

A common technique used when defining Items with stateless/static attributes, is to define the item definitions on the special setup variable. Which can be done either by using JavaScript within a project’s Story JavaScript area…

setup.items = {
    'wolf-pelt': {
        name: "Wolf Pelt",
        price: 12
    },
    'wolf-fang': {
        name: "Wolf Fang",
        price: 12
    }
};

…or by using macros within the project’s StoryInit special Passage…

<<set setup.items to {
    'wolf-pelt': {
        name: "Wolf Pelt",
        price: 12
    },
    'wolf-fang': {
        name: "Wolf Fang",
        price: 12
    }
}>>

Whenever you need information about a specific item, you would use the item’s identifier to lookup the definition…

Selling: <<= setup.items['wolf-pelt'].name>> for <<= setup.items['wolf-pelt'].price>>

Adding an instance of an item to a container like a Bag or Inventory is done in one of two ways, depending on if that container can have multiple instances of the same item. Either way the item’s identifier is added to the container.

1: If each item in the container is unique, then an Array could be used like so…

<<set $bag to []>>
<<run $bag.push('wolf-pelt')>>
<<run $bag.push('wolf-fang')>>

…which allows a loop to be used to list unique items in that Array…

Bag contains:
\<<for _id range $bag>>
    <<= setup.items[_id].name>>
\<</for>>

2: If one of more instances of an item can be stored in the container, then a Generic Object can be used…

<<set $bag to {}>>
/* bag has 2 pelts and 5 fangs */
<<set $bag['wolf-pelt'] to 2>>
<<set $bag['wolf-fang'] to 5>>

A loop can also be used to list the items in that Generic Object, but it will need to use a slightly different format, as the Generic Object has both identifiers & quantities (aka names & values).

Bag contains:
<<for _id, _amount range $bag>>
    <<= setup.items[_id].name>> x _amount
\<</for>>
1 Like

I have two answers for you. First,

And second, no one’s ever taught me how to use generic objects before, and the only way I’ve learned to make variables with subvariables inside of them was with an array. I went back to the documentation to look after your response, and I’m not finding a great explanation for how to make generic objects there, just ways they can be used once they’re built.

I was using arrays to make it easy to transfer data from a constant (wolf) to a variable (Foe1) so that the entire stat range copies over. I honestly had no idea generic objects were a thing, or how to use them.

So, I tried this, along with everything else you’d suggested, and this was the only thing that wasn’t working quite right.

I didn’t want the post-battle screen listing every item in the game with a whole bunch of zeroes to scroll through while looking for anything greater than zero. Of course rats aren’t going to drop wolf fangs, and I don’t want separate screens for every battle. And if you face two rats and a wolf, I don’t want to make a new passage for every combination of three opponents.

So, here’s my solution:

You received the following:

<<for _id, _amount range $Drops>>\
<<if _amount gt 0>>\
<<= setup.items[_id].Name>> x _amount
<</if>>\
<</for>>\
\

I don’t need to be told that I found Wolf Pelt x 0, the fact that it didn’t show up is proof enough that it’s not there. Just a simple check to filter out all of the zeroes.

Code works as intended, now.

1 Like

Just for ref, twine sugarcube is built off of javascript (it’s “sugary”, a programming term for “simplified” or “easier to write in”).

Therefore, for a lot of things you won’t find in the sugarcube documentation, you can try looking for javascript documentation/tutorials on it. :slight_smile:

Here’s a tutorial on javascript objects.

3 Likes