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

(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.

Aliases: (for:)


break

(break:) stops the enclosing (each:)/(for:) loop immediately. See (continue:) for the companion. Both affect only the innermost enclosing loop, work inside (if:) branches within the loop, and are ignored outside a loop (with a dev-build warning and a validator warning).

continue

(continue:) skips the rest of the current loop iteration and moves to the next. See (break:).

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

(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

(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

(query: namespace [, where, ns.key is value] [, sort, "key" [desc]] [, select, "key"] [, distinct] [, limit, N]). Expression macro. A composable query with optional filtering, sorting, projection, and a result cap. Returns an array of IDs (or, with select, an array of property values).

The first argument is the namespace. where predicates use the same form as (filter:). The pipeline runs in order: where → sort → select → distinct → limit.

  • select, "key": projects each matching entry to that property's value instead of returning its ID. Works across flat namespaces ($inv, $container.id), nested namespaces ($npc), and any custom namespace registered with (ns-nested:).
  • distinct: drops duplicate results (first occurrence kept). Most useful with select.
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))

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

// Distinct item types present in inventory: ideal for building tabs
(set: _types to (query: $inv, select, "type", distinct))   // e.g. ["weapon", "consumable"]

// Distinct locations NPCs are currently in
(set: _places to (query: $npc, select, "location", distinct))

ns-nested (GameInit only)

(ns-nested: "name") 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

(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. To remove a value use (remove: $arr, value), and (count: $arr) returns an array's length; there are no separate arr-* mutation macros.

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

(add: $player.events, "met_kyle")     // append
(remove: $player.events, "met_kyle")  // remove first occurrence
(if: (count: $player.events) >= 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 (append, remove), use (add:) and (remove:) (see Arrays above).

All are expression macros.


arr

(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

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.

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

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

Aliases: (either:) reads naturally for one-off random prose where no array variable is needed.


weighted

(weighted: value, weight, value, weight, ...) returns one value chosen at random, each with probability proportional to its weight. This is a weighted random table for loot drops, random encounters, or any roll where outcomes aren't equally likely. Weights are any positive numbers and need not sum to 100. A zero or negative weight is never chosen, and the macro returns null when no pair has a positive weight.

ana
(set: _drop to (weighted: "common", 70, "rare", 25, "legendary", 5))

// Weights can be variables, so odds can shift with game state:
(set: _encounter to (weighted: "wolf", 50, "bear", $world.danger, "nothing", 30))

first

(first: $array) returns the first element, or null for an empty array.

ana
(set: _opening to (first: $player.events))

last

(last: $array) returns the last element, or null for an empty array.

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

nth

(nth: $array, n) returns the element at 1-indexed position n. Returns null if out of range.

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

shuffle

Returns a new randomly shuffled copy. Accepts either a single array argument or a series of inline values. The original is unchanged.

ana
(shuffle: $array)
(shuffle: val1, val2, ...)

(set: _deck to (shuffle: $game.deck))

sort

Returns a new sorted copy in ascending order: numbers numerically, strings lexicographically. Accepts either a single array argument or a series of inline values. The original is unchanged.

ana
(sort: $array)
(sort: val1, val2, ...)

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

reverse

Returns a reversed copy of an array, or a reversed string. The original is unchanged.

ana
(reverse: $array)
(reverse: string)

(set: _reversed to (reverse: $player.inventory))
(set: _flip to (reverse: "Steve"))           // → "evetS"

unique

(unique: $array) returns a new copy with duplicate values removed, preserving first occurrence order.

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

Aliases: (dedupe:)


slice

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

ana
(slice: $array, from, to?)
(slice: string, from, to?)

(set: _middle to (slice: $player.events, 2, 4))
(set: _tail to (slice: $player.events, (count: $player.events) - 2))
(set: _sub to (slice: "hello", 2))            // → "ello"

range

Builds an array of consecutive integers from from to to, inclusive on both ends. Counts down instead of up when from is greater than to. Non-integer endpoints are truncated toward zero.

ana
(range: from, to)

(set: _nums to (range: 1, 10))      // → [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
(set: _down to (range: 5, 1))       // → [5, 4, 3, 2, 1]
(each: _i in (range: 1, 3))[Floor _i. ]

Pairs naturally with (each:) for count-based loops. For repeating a string a fixed number of times, use (repeat:) instead.


length

Returns the character count of a string or the entry count of an array.

ana
(length: value)

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

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.

ana
(join: val1, val2, ...)

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

Aliases: (concat:)

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

(min: a, b) returns the smaller of two numbers.

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

max

(max: a, b) returns the larger of two numbers.

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

clamp

(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

(round: value) rounds to the nearest integer (0.5 rounds up).

ana
(set: _display to (round: $player.health))

floor

(floor: value) rounds toward negative infinity.

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

ceil

(ceil: value) rounds toward positive infinity.

ana
(set: _pages to (ceil: $items / 10))

abs

(abs: value) returns the absolute value of a number.

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

sum

(sum: ...) totals its numbers. 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

avg

(avg: ...) returns the mean of its numbers (0 for an empty series). Accepts either a single array argument or a series of inline numbers.

ana
(set: _mean to (avg: $player.scores))

convert

(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. To measure a string's length use (length:); to take a substring use (slice:); to reverse one use (reverse:); those macros work on both strings and arrays.


lowercase

(lowercase: str) converts a string to lower case.

ana
(set: _tag to (lowercase: $world.event))

uppercase

(uppercase: str) converts a string to upper case.

ana
(set: _shout to (uppercase: $world.event))

proper

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

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

trim

(trim: str) strips leading and trailing whitespace.

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

split

(split: str, sep) splits a string into an array by the separator.

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

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

collapse

(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 (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

(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

(str: value) converts any value to its string representation.

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

num

(num: str) parses a string as a number, returning NaN if the string is not a valid number.

ana
(set: _parsed to (num: "3.14"))