Scope - ThePix/QuestJS GitHub Wiki

Determining what item should be listed when is very complicated once you have a sophisticated world-model. Some issues are:

  • A room within a room (for example, the player might be stood on a chair that is inside a cage, which in turn is in a hall)
  • An item that is in more than one room (for example, a door is likely to be in two rooms)
  • Items in containers that could be open or closed, transparent or opaque, or inside another container
  • Items held or worn by NPCs
  • Items that can or cannot be found when it is dark
  • Items considered scenery
  • Some instances of a countable could be in a container, whilst others are in another room and still more are held.

My first attempt at this involved iterating through every object, and for each object is turn, iterating through everything to see if it was accessible. For 100 objects, that would be 10,000 tests, which might be done four or five times a command. I have now abandoned that for a system that involves storing a snapshot of the scope at a moment in time. This has a very real risk that it could expire if not updated when the world changes. However, tests are now running at about 2.8 ms each, rather than about 4.2 ms.

You can call game.update() to ensure scope is up-to-date after moving the player or an item, changing lighting, opening/closing, etc. if this is going to be an issue. This is called at the end of each turn anyway to ensure the side pane is right; you would only need to call it yourself if you are modifying the world and then have some further processing to do.

world.scope

Items that are in scope will be in the list world.scope which is re-built when a snapshot is taken. This will include the room itself, as of version 1.0.1.

By default, commands are restricted to items in world.scope, but you can change that for a specific command (see here).

Methods on items

getContents(situation) will return the child objects of a room, container, surface or NPC, i.e., all items that return true for isAtLoc with the given situation.

isAtloc(loc, situation) will return true if the object is at the named location. This can be modified for items in odd situations. The background object returns true because it is everywhere, a component returns true if its parent is here.

canReachThrough() will return true if you can access the contents (the contain is open, or whatever)

canSeeThrough() will return true if you can access the contents (the contain is open, or whatever)

scopeSnapshot(visible) sets the scope snapshot for this and its contents

Scope functions

The primary scope function is scopeBy. It takes a function parameter that determines if an item is to be included. Here is an example that will get all items with an "activeEffects" attribute.

const objs = scopeBy(function(el) { return el.activeEffects !== undefined })

JavaScript has a shortcut that is less typing, but less clear what is going on. This example is the same as the one above:

const objs = scopeBy(el => el.activeEffects !== undefined )

There are some built-in scope functions too.

// Returns an array of objects the player can currently reach and see.
function scopeReachable()

// Returns an array of objects held by the given character.
function scopeHeldBy(chr, situation = world.PARSER)

// Returns an array of objects at the player's location that can be seen.
function scopeHereListed()

// Returns an array of objects at the player's location that can be seen.
function scopeHereParser()

// Returns an array of NPCs at the player's location (excludes those flagged as scenery).
function scopeNpcHere(ignoreDark)

// Returns an array of NPCs at the player's location (includes those flagged as scenery).
function scopeAllNpcHere(ignoreDark)

The parser also has its own set of scope functions.

How Does scopeSnapshot work?

The scope revolves around two attributes, scopeStatus and scopeStatusForRoom, so the first thing scopeSnapshot does is delete these for all objects.

Next we find the highest level room the player is in and is reachable (so if the player is in a barred cage, in a lab, the lab is reachable too). Each room has scopeStatusForRoom flagged as REACHABLE. We then call scopeSnapshot on the room object, with false to set the reachable objects.

Then we do the same thing again for visible, rather than reachable, and call scopeSnapshot on the highest room object, with true.

The scopeSnapshot function takes a boolean, true if we are checking visible, false for reachable. It firstly checks to see if scopeStatus is already set - if it is, this object has already been done, so nothing more happens. If not, this object is set to be visible or reachable.

If this is a container or has components, then we need to check though them, and the rest of the function does that, calling scopeSnapshot for any contained objects as appropriate.