Removing One Object during a foreach loop

Ok, so I am trying to allow my player to use a rope to descend downward in various places, and I will be cloning ropes, so I needed a way to check if the player has a rope, and then use only one each time they descend. I set up a variable on the player object called "hasrope". Each time the player takes a rope, I call the script game.pov.hasrope = game.pov.hasrope + 1. The code I am in the process of creating for this functionality is as follows.

if (game.pov.hasrope >= 1) {
  MoveObject (player, WellCave1)
  game.pov.hasrope = game.pov.hasrope - 1
  if (game.pov.hasrope = 0)
    foreach (item, ScopeReachableInventory()) {
      if (StartsWith (item.name, "rope")) {
        RemoveObject (item)
      }
    }  
  else if (game.pov.hasrope >= 1) {
    foreach (item, ScopeReachableInventory()) {
      if (StartsWith (item.name, "rope")) {
        
      }
    }
  }
}
else {
  msg ("You'll need a rope to climb down into the well.")
}

The first if statement checks if the player has a rope. The script then subtracts one from the "hasrope" total, then checks to see if it's 0. If so, I know the player only has one rope, and it will go through the inventory and delete anything that starts with "rope". The trouble comes in when a player starts with more than one rope. The else if statement will fire if the player started with 2 or more ropes. At this point, I'd like it go through the inventory and remove just one rope, but I'm not entirely sure how to do that. Perhaps there's a different way to go about this where I don't need the "hasrope" attribute. Maybe I could just check for ropes in my inventory to trigger my movement script and then remove the object from the inventory somehow, but I'm not sure how I would achieve this either.


You're using StartsWith (item.name, "rope") to find an object called rope and its clones?

If you're cloning it using the core functions (CloneObject, CloneObjectAndMove, etc), then you could do PickOneObject (FilterByAttribute (ScopeReachableInventory(), "prototype", rope)) to pick a random rope from the player's inventory.

Otherwise, you probably want to use a boolean attribute to track if you already found one. For example:

    found = false
    foreach (item, ScopeReachableInventory()) {
      if (not found) {
        if (StartsWith (item.name, "rope")) {
          RemoveObject  (item)
          found = true
        }
      }
    }

I'm wondering when you increase the hasrope total. If you increase it when the player finds a rope, then they could put the rope in a closed container and then keep on reusing it; it wouldn't be removed because it isn't in the reachable inventory. If possible, it makes more sense to actually check if there is a rope there, rather than keeping a separate count. So your function would be:

ropes = FilterByAttribute (ScopeReachableInventory(), "prototype", rope)
if (ListCount (ropes) >= 1) {
  RemoveObject (PickOneItem (ropes))
  MoveObject (player, WellCave1)
}
else {
  msg ("You'll need a rope to climb down into the well.")
}

If your ropes aren't all clones of an original rope object, you could modify that to use some other attribute to identify ropes.


This is perfect! Thank you so much again. Much more simple than the way I was trying to go about it. The hasrope attribute worked great when my game only contained one rope and one place to use it, but now that things are growing, I've had to reconsider many of the systems I had in place. I did have to tweak that script a bit. I used the alias instead of the prototype name as some of the objects are clones when bought from a store, but others are strategically placed in the world. I also had to change PickOneItem to PickOneObject as I don't think Quest recognizes the item version. Here's the full script for anyone that might find it useful:

ropes = FilterByAttribute (ScopeReachableInventory (), "alias", "rope")
msg (ropes)
if (ListCount (ropes) >= 1) {
  MoveObject (player, WellCave1)
  RemoveObject (PickOneObject (ropes))
}
else {
  msg ("You'll need a rope to climb down into the well.")
}

I also wanted to take a moment to let you know that your help has been invaluable. I saw in a forum post quite a while back that you had written and are selling some books, but I can't seem to find that post again. If you wouldn't mind posting a link to your work, I'd love to support you somehow.


Sorry, guess I wasn't paying attention. Yes, PickOneObject is the correct function name.

Using prototype is the usual way to handle clones - the CloneObject family of functions all set a clone's prototype to the original object unless it has one already. If you've got some strategically placed in the world, you could give them a prototype attribute manually - this is especially useful if you're using things like a stacking system.

An alternative would be using types, in which case you can use FilterByType; or having something in your start script that clones the rope to all the places they're needed. These methods both have the advantage that if you want to change the rope's price, weight, displayverbs, or similar, you don't need to go through copying the change to all the ones that are out in the world.

Using the alias to identify an item works - but could lead to bugs if you (or someone else) comes to translate your game to another language; because you'd have to change it every place it's referenced.

You could also choose to use ObjectListItem(ropes, 0) to get the first rope in the list, rather than PickOneObject (ropes) to pick one at random - if all your ropes are identical, it doesn't really matter which one you pick; if you're dealing with items which aren't completely identical (like in one of my games there were "pink flower", "blue flower", "green flower", etc - they fill the same purpose but have different descriptions), the two methods can give a slightly different feel to the game.


In answer to the original question, in case you ever have another case where you want a foreach loop to stop after it found a match. Rather than using the boolean, an alternative I use sometimes is to put the loop in a function. Then you can use return() to end the function. Most programming languages have a break, continue, or last command that just exits a loop, but I don't think Quest does.


I also wanted to take a moment to let you know that your help has been invaluable. I saw in a forum post quite a while back that you had written and are selling some books, but I can't seem to find that post again. If you wouldn't mind posting a link to your work, I'd love to support you somehow.

Thanks :) It's been great getting a few extra sales in Dec and Jan, so I really appreciate the support. Reviews would be even better, if you like what I've come up with. Most of my books are at https://author.to/angelwedge


Log in to post a reply.

Support

Forums