Skip to content

Loops, Arrays, Numbers & Strings

Iterating over collections and engine namespaces, and the value macros for working with arrays, numbers, and text.

Loops & Namespace Queries

Ana provides a loop construct and three collection macros ((ids:), (filter:), and (query:)) that work across all engine namespaces: $npc, $inv, $container, and any custom namespace registered with (ns-nested:). These macros always return arrays, so they pair naturally with (each:).

All three macros return arrays you can iterate with (each:).


(each: _var in collection)[body]

Loops over every item in an array, binding the current item to a temp variable. The block renders once per iteration.

ana
(set: _items to (ids: $inv))
(each: _id in _items)[
    (get: $item, _id, "name")
]

The collection can be any expression that returns an array: a variable, a filter result, or any other expression.


(break:) / (continue:)

Loop control inside an (each:) body. (break:) stops the loop immediately; (continue:) skips the rest of the current iteration and moves to the next. Both affect only the innermost enclosing loop, and both work inside (if:) branches within the loop. Used outside a loop, they are ignored (with a dev-build warning and a validator warning).

ana
// First weapon in a large inventory — stop as soon as we find one
(each: _id in (ids: $inv))[
    (if: (get: $item, _id, "type") is "weapon")[
        You ready your (get: $item, _id, "name").
        (set: $world.drawnWeapon to _id)
        (break:)
    ]
]

// Skip locked entries
(each: _id in (ids: $container.chest))[
    (if: (get: $item, _id, "locked"))[(continue:)]
    You take the (get: $item, _id, "name").
]

(ids: namespace)

Expression macro. Returns an array of all IDs in the given namespace.

ana
(ids: $npc)                  // all NPC IDs
(ids: $inv)                  // all item IDs in carry inventory
(ids: $container.chest)      // all item IDs in a specific container
(ids: $faction)              // all IDs in a custom nested namespace

(filter: ns.key is value [, ns.key2 is value2 ...])

Expression macro. Returns an array of IDs where all predicates match (AND logic). Multiple predicates are supported for 2-part ns.key forms.

ana
// NPC filter
(set: _atBar to (filter: $npc.location is "bar"))
(each: _id in _atBar)[
    (get: $npc, _id, "name") nurses a drink.
]

// Multi-predicate AND
(set: _targets to (filter: $npc.location is "bar", $npc.gender is "female"))

// Inventory filter — returns item IDs matching a property
(set: _weapons to (filter: $inv.type is "weapon"))
(each: _id in _weapons)[
    You're carrying a (get: $item, _id, "name").
]

// Multi-predicate inventory filter
(set: _heavy to (filter: $inv.type is "weapon", $inv.weight > 5))

// Container filter — 3-part form ($container.id.key), single predicate only
(set: _potions to (filter: $container.shop_goods.type is "potion"))

// Custom namespace
(set: _friendly to (filter: $faction.standing > 50))

Filter operators: is, is not, >, <, >=, <=.

npc.location here is a predicate, not a variable. $npc is a nested namespace keyed by id; the actual stored values live at $npc.bartender.location, $npc.guard.location, and so on. The npc.location is "bar" form inside (filter:)/(query:) means "for every npc id, test its location key"; it does not read a single $npc.location value (there is no such variable). This is how you iterate a nested namespace: (filter:) / (query:) walk every id for you, or use (each: _id in (ids: $npc)) and read each with (get: $npc, _id, "location").


(query: namespace [, where, ns.key is value] [, sort, "key" [desc]] [, limit, N])

Expression macro. A composable query with optional filtering, sorting, and a result cap. Returns the same array format as (ids:) and (filter:).

The first argument is the namespace. where predicates use the same form as (filter:).

ana
// All NPCs in the bar, sorted by name
(set: _bar to (query: $npc, where, $npc.location is "bar", sort, "name"))

// Top 3 highest-level hostile NPCs
(set: _enemies to (query: $npc, where, $npc.faction is "hostile", sort, "level", desc, limit, 3))

// All NPCs sorted, no filter
(set: _roster to (query: $npc, sort, "name"))

// Inventory: highest-damage weapons, cap 5
(set: _top to (query: $inv, where, $inv.type is "weapon", sort, "damage", desc, limit, 5))

(ns-nested: "name") (GameInit only)

Registers a new nested namespace that behaves identically to npc: it supports (declare: $name.id, ...), (ids: name), (filter: name.key is value), and (query: name, ...).

ana
(ns-nested: "faction")

(declare: $faction.guild, name, "Merchant Guild", standing, 50)

(ids: $faction)                                                          // ["guild"]
(filter: $faction.standing > 40)                                         // ["guild"]
(query: $faction, where, $faction.standing > 40, sort, "name")

Note: there is no (ns-flat:) macro. Flat namespaces ($world.timeOfDay, $player.gold) work automatically the moment you (declare:) a variable in them; no registration call is needed. See Variable depth for the three namespace depths.


Quick reference by namespace

Namespace(ids:)(filter:) predicateNotes
npc(ids: $npc)$npc.key is valueAny (npc-define:) / (declare: $npc.id, ...) entry
inventory(ids: $inv)$inv.key is valueItem template IDs
container(ids: $container.chest)$container.chest.key is value3-part, single predicate
custom(ids: $faction)$faction.key is valueAfter (ns-nested: "faction")

Events are plain array variables; loop directly with (each: _ev in $npc.bartender.events)[...]

Arrays

Arrays are declared with (declare: $variable, []) in GameInit. Use arrays for ordered lists of strings or numbers: visited locations, collected clues, event flags.


(add: $arrayVariable, value)

Appends a value to the end of an array. This is the same (add:) macro used for numeric variables; it detects the array type automatically.

ana
(declare: $player.events, [])

(add: $player.events, "met_kyle")
(add: $player.events, "found_note")

Array aliases: (arr-push:), (arr-remove:), (arr-count:)

These are aliases of the polymorphic verbs; prefer the unprefixed form, which is the engine's house style:

Prefixed aliasPreferred form
(arr-push: $arr, v)(add: $arr, v); append to an array
(arr-remove: $arr, v)(remove: $arr, v); remove first occurrence (no-op if absent)
(arr-count: $arr)(count: $arr); array length

The aliases are retained for readability in array-heavy code where the array nature is worth signalling, but they do exactly what (add:)/(remove:)/(count:) do on an array variable.

ana
(declare: $visited, [])
(add: $visited, "bar")          // preferred
(arr-push: $visited, "mill")    // alias — identical effect
(remove: $visited, "bar")
(if: (count: $visited) >= 5)[You've been around.]

contains / not contains / does not contain operators

Built into the condition syntax. Checks whether an array or string includes a value. No macro needed.

ana
(if: $visited contains "mill")[
    You know the layout.
]

(if: $visited not contains "mill")[
    You've never been out there.
]

(if: $visited does not contain "mill")[
    You've never been out there.
]

Event flags via arrays: declare an event array, push tags to it, check with contains:

ana
// GameInit
(declare: $player.events, [])

(add: $player.events, "found_note")

(if: $player.events contains "found_note")[
    You remember that report.
]

Traits: just a string array on the character namespace:

ana
// GameInit
(declare: $player.traits, [])

(add: $player.traits, "charming")

(if: $player.traits contains "charming")[
    Your smile puts them at ease.
]

(if: $player.traits does not contain "fearless")[
    Something gives you pause.
]

Array Macros

Functional array operations; all return new arrays or single values. None of these mutate the original variable. For array mutation (push, remove), use (add:) and (arr-remove:) (see Arrays above).

All are expression macros.


(arr: val1, val2, ...)

Builds a new array from a series of inline values. (arr:) with no arguments returns an empty array.

ana
(set: $player.inventory to (arr: "sword", "shield", "potion"))
(set: _empty to (arr:))

(pick: $array) / (pick: val1, val2, ...)

Returns a random value. Pass a single array to pick one of its elements, or a series of inline values to pick among them. Returns null for an empty array or empty series. (either:) is an alias.

ana
(set: _greeting to (pick: $npc.bartender.greetings))   // from an array
(pick: "He nods.", "He shrugs.", "He grunts.")          // from inline values

(either: val1, val2, ...)

Alias of (pick:); reads naturally for one-off random prose where no array variable is needed.

ana
(either: "He nods.", "He shrugs.", "He grunts.")

(set: _mood to (either: "cheerful", "neutral", "gruff"))

(arr-first: $array) / (arr-last: $array)

Returns the first or last element. Returns null for an empty array.

ana
(set: _latest to (arr-last: $player.events))

(arr-nth: $array, n)

Returns the element at 1-indexed position n. Returns null if out of range.

ana
(set: _second to (arr-nth: $player.events, 2))

(arr-shuffle: $array)

Returns a new randomly shuffled copy. The original array is unchanged.

ana
(set: _deck to (arr-shuffle: $game.deck))

(arr-sort: $array)

Returns a new sorted copy in ascending order. Numbers sort numerically; strings sort lexicographically. The original array is unchanged.

ana
(set: _sorted to (arr-sort: $player.scores))

(arr-reverse: $array)

Returns a new reversed copy. The original array is unchanged.

ana
(set: _reversed to (arr-reverse: $player.inventory))

(arr-unique: $array)

Returns a new copy with duplicate values removed, preserving first occurrence order.

ana
(set: _visited to (arr-unique: $player.locations))

(arr-slice: $array, from, to?)

Returns a sub-array from 1-indexed position from to to (inclusive). If to is omitted, slices to the end of the array. The original array is unchanged.

ana
(set: _last3 to (arr-slice: $player.events, (arr-count: $player.events) - 2))
(set: _middle to (arr-slice: $player.events, 2, 4))

(sort: ...) / (shuffle: ...)

Polymorphic versions of (arr-sort:) / (arr-shuffle:). Each accepts either a single array argument or a series of inline values, and returns a new sorted/shuffled copy.

ana
(set: _ranked to (sort: $player.scores))   // array form
(set: _ranked to (sort: 3, 1, 2))           // inline series → [1, 2, 3]
(set: _order to (shuffle: $game.deck))

(length: value) / (arr-length: $array)

(length:) returns the character count of a string or the entry count of an array. (arr-length:) is the array-only form (an alias of (arr-count:)); use it when you want to assert the argument is an array.

ana
(set: _chars to (length: $player.name))      // string → character count
(set: _items to (length: $player.inventory)) // array → entry count
(set: _items to (arr-length: $player.inventory))

(join: ...) / (concat: ...) / (arr-join: ...)

Concatenates strings, or merges arrays into one new array. If every argument is an array, the result is a merged array; otherwise the arguments are coerced to strings and concatenated. (concat:) and (arr-join:) are aliases.

ana
(set: _full to (join: "Hello, ", $player.name, "!"))   // strings → one string
(set: _all to (join: $party, $reserves))               // arrays → one merged array

This is distinct from (collapse:), which joins the elements of a single array into a string with a separator.

Number Macros

Utility math functions. All are expression macros; use them inside (set:), conditions, or anywhere a value is expected.


(min: a, b) / (max: a, b)

Returns the smaller or larger of two numbers.

ana
(set: _capped to (min: $player.health, 100))

(if: (max: $rel.bartender, 0) > 50)[
    He's friendly.
]

(clamp: value, min, max)

Constrains a value to the range [min, max]. Returns min if below, max if above, otherwise value unchanged.

ana
(set: _safe to (clamp: $player.health, 0, 100))
(set: _vol to (clamp: _rawVol, 0.0, 1.0))

(round: value) / (floor: value) / (ceil: value)

Rounding functions. (round:) rounds to the nearest integer (0.5 rounds up). (floor:) rounds toward negative infinity. (ceil:) rounds toward positive infinity.

ana
(set: _display to (round: $player.health))
(set: _slots to (floor: $player.stamina / 10))

(abs: value)

Returns the absolute value of a number.

ana
(set: _diff to (abs: $player.health - $player.maxHealth))

(sum: ...) / (avg: ...)

(sum:) totals its numbers; (avg:) returns their mean (0 for an empty series). Each accepts either a single array argument or a series of inline numbers.

ana
(set: _total to (sum: $player.scores))   // array form
(set: _total to (sum: 10, 20, 30))        // inline → 60
(set: _mean to (avg: $player.scores))

(convert: value, fromUnit, toUnit)

Converts a numeric value between units in the same category. Supported categories:

  • Length: mm, cm, m, km, in, ft, yd, mi
  • Mass: mg, g, kg, oz, lb, st
  • Volume: ml, l, floz, cup, pt, qt, gal
  • Temperature: c, f, k

Unit names are case-insensitive and accept common spellings (e.g. "feet", "pounds", "celsius"). Converting between incompatible categories throws an error.

ana
(set: _cm to (convert: 6, "ft", "cm"))       // → 182.88
(set: _f to (convert: 100, "c", "f"))         // → 212

String Macros

All are expression macros. Use inside (set:) or anywhere a value is expected. The dash-notation names (str-length, str-from, etc.) use hyphens to distinguish them from variable paths.


(lowercase: str) / (uppercase: str) / (proper: str)

Case conversion. (proper:) capitalizes the first letter of each word.

ana
(set: _name to (proper: $player.name))    // "the old mill" → "The Old Mill"
(set: _tag to (lowercase: $world.event))

(trim: str)

Strips leading and trailing whitespace.

ana
(set: _clean to (trim: _rawInput))

(str-length: str)

Returns the number of characters in the string.

ana
(if: (str-length: $player.name) is 0)[
    You haven't entered a name.
]

(str-from: str, n) / (str-to: str, n)

Substring operations using 1-indexed positions.

  • (str-from: str, n): returns from position n to the end.
  • (str-to: str, n): returns from the start up to and including position n.
ana
(str-from: "hello", 3)    // → "llo"
(str-to:   "hello", 3)    // → "hel"
(str-from: "hello", 1)    // → "hello"

(str-split: str, sep)

Splits a string into an array by the separator.

ana
(set: _parts to (str-split: "a,b,c", ","))
// → ["a", "b", "c"]

(each: _tag in (str-split: $player.tagString, " "))[
    _tag
]

(collapse: $array, sep?)

Collapses an array's elements down into one string, joined by sep. If sep is omitted, elements are concatenated with no separator. This is the inverse of (str-split:).

ana
(set: _arr to ["a","b","c"])
(set: _out to (collapse: _arr, "-"))    // → "a-b-c"
(set: _out to (collapse: _arr))         // → "abc"

(Not to be confused with (join:), which concatenates separate string/array arguments rather than the elements of one array.)


(repeat: count, value)

Returns a string with value repeated count times. count is floored and clamped to a minimum of 0.

ana
(set: _bar to (repeat: 10, "="))   // → "=========="
(set: _dots to (repeat: $level, "."))

(str: value) / (num: str)

Type coercion. (str:) converts any value to its string representation. (num:) parses a string as a number, returning NaN if the string is not a valid number.

ana
(set: _label to (str: $player.gold) + " gold")
(set: _parsed to (num: "3.14"))