Appearance
Events, Tags, Status & Dice
Event flags and passage-tag-driven events, applying status effects, and rolling dice.
Event Arrays
Events track things that have or haven't happened. Events are array variables declared on whichever namespace owns them.
Declare event arrays in GameInit like any other variable. Use (add:) to record events and contains to check them.
ana
// GameInit (_init.ana)
(declare: $player.events, [])
(declare: $npc.bartender.events, [])
(declare: $world.events, [])
// Record an event:
(add: $player.events, "met_kyle")
(add: $npc.bartender.events, "punched")
(add: $world.events, "found_note")
// Check an event:
(if: $player.events contains "met_kyle")[
He remembers you.
]
(if: $npc.bartender.events does not contain "punched")[
He seems relaxed.
]
// Loop over all events:
(each: _ev in $npc.bartender.events)[
Bartender event recorded: (_ev)
]
// Clear all events (reset the array):
(set: $player.events to [])Why this design? Events are just boolean membership checks on an array. The contains operator already exists for arrays, and arrays are already a saveable StateValue type.
Scope naming conventions:
| Variable | Use for |
|---|---|
$player.events | Things that happened involving the player |
$npc.bartender.events | Things that happened with a specific NPC |
$world.events | Major story beats that persist forever |
$world.todayEvents | Events that reset at day end |
To "scope-clear" a group of events (for example, resetting daily events at dawn), set the array back to []:
ana
:: Player_OnDayAdvance @on(dayAdvance)
(set: $world.todayEvents to [])
(add: $world.days, 1)
(notify: "A new day begins.")Tag & Event Architecture
Passage tags and @on(event) handlers work together to let passages react to game events automatically, with no manual (trigger:) calls needed for standard engine events.
Passage Tags
Tags are added to a passage header in brackets:
ana
:: Bar_Main [location bar]
:: Chapter1 [chapter]
:: BlacksmithShop [location shop]
:: GoodEnding [ending]Built-in semantic tags (these fire named events and update tracking variables):
| Tag | Event fired | Side effect |
|---|---|---|
location | locationEnter | Sets $world.location to the passage name |
chapter | chapterStart | Sets $world.chapter to the passage name |
shop | shopEnter | None |
ending | endingReached | None |
Custom tags fire an event with the tag name verbatim. A passage tagged [puzzleComplete] fires a puzzleComplete event.
Multiple tags are allowed. A passage tagged [location shop] fires both locationEnter and shopEnter, and sets $world.location.
@on(eventName): event handler passages
Passage marked @on(eventName) executes automatically whenever that event fires.
ana
:: _On_LocationEnter @on(locationEnter)
(feedback: "You entered $world.location")
:: _On_ShopBuy @on(shopBuy)
(if: $world.questFlags.barterMaster is false)[
(add: $player.barters, 1)
]Context temp variables are set for each event and available for the duration of the handler execution.
System Event Reference
| Event | Fired when | Context vars |
|---|---|---|
gameStart | New game begins (after @system(init) runs) | None |
gameSave | Game saved successfully | _slot |
locationEnter | Navigation to a [location] passage | None |
chapterStart | Navigation to a [chapter] passage | None |
shopEnter | Navigation to a [shop] passage | None |
endingReached | Navigation to an [ending] passage | None |
questStart | (quest-start:) succeeds | _questId |
questAdvance | (quest-advance:) moves to next stage | _questId, _stageId |
questComplete | (quest-complete:) or (quest-turnin:) fires | _questId |
questFail | (quest-fail:) fires | _questId |
equipChange | Item equipped or unequipped via (add:)/(remove:) | _slot, _itemId |
statusApply | (status-apply:) activates a status | _statusId |
statusExpire | A duration-based status expires via (time-advance:) | _statusId |
shopBuy | (shop-buy:) completes a purchase | _shopId, _itemId, _cost |
shopSell | (shop-sell:) completes a sale | _shopId, _itemId, _price |
npcLoad | (load-npc:) loads an NPC | _npcId |
dayAdvance | Day count rolls over in (time-advance:) | None |
periodChange | Time period changes in (time-advance:) / (time-set-period:) | None |
Custom tags add their own events verbatim. A passage tagged [myEvent] fires myEvent on navigation.
Note: questComplete and questFail also expose _questId as a temp var. (quest-event-id:) is still available for backwards compatibility, but _questId is the preferred approach going forward.
$world.location and $world.chapter
These variables are set automatically when navigating to tagged passages. They do not require (declare:); the engine declares them lazily on first use.
ana
:: _On_LocationEnter @on(locationEnter)
(feedback: "Entering $world.location")Custom events via (trigger:)
Fire any event manually:
ana
(trigger: "puzzleComplete")
(trigger: "bossDefeated")This calls all @on(puzzleComplete) / @on(bossDefeated) handlers, with no context vars. To pass context, set globals before triggering.
Status Effects
Status effects are named conditions that can modify stats, deal/heal damage over time, or simply mark a state for the game to check. Three modifier types cover the full range of common scenarios.
$variable path: $status.statusId: a boolean flag, true if the status is active, false otherwise. Readable inline: (if: $status.poisoned)[You are poisoned!]. Updated by (status-apply:), (status-remove:), and expiry.
Modifier types
| Kwarg | Behavior | Apply | Remove / Expiry |
|---|---|---|---|
effect | Flat buff/debuff; stat changes while active | Delta applied immediately | Delta reversed |
dot | Recurring effect; delta applied each (time-advance:) tick | No immediate change | Nothing reversed; stat stays wherever ticks left it |
| (none) | Flag only, no stat change | None | None |
Multiple effect and dot kwargs can appear in the same (status-define:) call.
(status-define: "id", [kwargs])
Declares a status effect. Call in GameInit.
Kwargs:
name, "...": display name used in feedback messagesdescription, "...": tooltip or description texteffect, $var, delta: flat modifier, applied on activate, reversed on remove/expirydot, $var, delta: recurring modifier, applied each tick, never reversedduration, N: auto-removes after N(time-advance:)steps; omit or-1for indefiniteduplicate, mode: behavior when(status-apply:)is called while already active:ignore(default),reset,extend-full, orextend-half; overrides the engine-wide default set by(status-duplicate:)
ana
// Flat buff: +10 persuasion while active, snaps back when removed
(status-define: "inspired",
name, "Inspired",
effect, $player.persuasion, 10,
duration, 5)
// Flat debuff: -20 stealth, indefinite — must be removed by game logic
(status-define: "compromised",
name, "Compromised",
description, "They know your face.",
effect, $player.stealth, -20)
// DoT: -10 health per tick; taking damage is permanent until expires or removed
(status-define: "poisoned",
name, "Poisoned",
description, "Toxin eating away at you.",
dot, $player.health, -10,
duration, 3)
// HoT: +5 health per tick; healing persists after the elixir wears off
(status-define: "healing_elixir",
name, "Healing Elixir",
dot, $player.health, 5,
duration, 6)
// Flag only: no stat change — game checks (status-has:) to gate actions
(status-define: "handcuffed",
name, "Handcuffed",
description, "You are unable to use your hands.",
duration, 2)(status-duplicate: mode)
Config macro. Sets the engine-wide default behavior when (status-apply:) is called on a status that is already active. Call in GameInit. Individual statuses can override this with the duplicate kwarg on (status-define:). The built-in fallback (if this macro is never called) is ignore.
| Mode | Behavior |
|---|---|
ignore | No-op; status is unchanged. "You can't be poisoned twice." |
reset | Remaining duration resets to the full defined value. |
extend-full | Full defined duration added to remaining duration (additive stacking). |
extend-half | Half the defined duration (floor) added to remaining duration. |
ana
(status-duplicate: reset) // most statuses refresh on re-apply(status-apply: "id")
Activates a status. For effect modifiers, applies the delta to the stat immediately. For dot modifiers, nothing changes yet; the recurring delta fires on future (time-advance:) calls. Pushes a feedback entry. When the status is already active, behavior follows the duplicate mode (per-status override or the engine-wide default; see (status-duplicate:)). Flat modifiers are not re-applied on re-activation; only the remaining duration is updated.
ana
(status-apply: "inspired") // +10 persuasion immediately
(status-apply: "poisoned") // no health change yet — first tick on next time-advance
(status-apply: "handcuffed") // just sets $status.handcuffed to true(status-remove: "id")
Deactivates a status. Reverses effect (flat) modifiers. dot modifier deltas already applied are not reversed; the stat stays wherever the ticks left it. Safe to call when the status is not active.
ana
(status-remove: "inspired") // persuasion drops back by 10
(status-remove: "poisoned") // stops future ticks; health damage already taken is permanent
(status-remove: "handcuffed") // clears $status.handcuffed(status-has: "id")
Expression macro. Returns true if the status is currently active, false otherwise.
ana
(if: (status-has: "poisoned"))[
Your hands tremble.
]
(if: (status-has: "handcuffed"))[
You can't reach the keys while cuffed.
(link: "Call for help")[(update: CallForHelp)]
](status-active:)
Expression macro. Returns an array of all currently active status IDs.
ana
(if: (status-active:) contains "poisoned")[Poisoned]
(each: _s in (status-active:))[
_s is affecting you.
](status-duration: "id")
Expression macro. Returns the remaining ticks before a status expires.
- Returns the remaining tick count (a number ≥ 1) while the status is active and has a finite duration.
- Returns
-1if the status is active but indefinite (nodurationset, orduration, -1). - Returns
0if the status is not currently active.
ana
(if: (status-has: "poisoned"))[
Poisoned — (status-duration: "poisoned") ticks remaining.
](status-name: "id")
Expression macro. Returns the display name of a status effect as defined by the name kwarg in (status-define:). Falls back to a proper-cased version of the ID if the status has not been defined (e.g., "badly-poisoned" → "Badly Poisoned").
ana
(each: _id in (status-active:))[
(status-name: _id)
]
// → "Poisoned", "Inspired", etc.(status-desc: "id")
Expression macro. Returns the description string of a status effect as defined by the description kwarg in (status-define:). Returns an empty string if the status is not defined or has no description. Useful for tooltips.
ana
(each: _id in (status-active:))[
(html: "<span title='" + (status-desc: _id) + "'>" + (status-name: _id) + "</span>")
]Duration, ticks, and DoT math
duration, N counts (time-advance:) steps. In cycle mode each step is one period; in linear mode each step is one hour.
For dot modifiers: when (time-advance: N) is called, the delta fires once per tick for however many ticks the status is still active. A status with duration, 2 that receives (time-advance: 5) fires its dot exactly 2 times (then expires), not 5. Stat bounds (min/max) are enforced on each write.
ana
// Poisoned at health 100, -10/tick, duration 3
(time-advance: 2) // → health 80 (2 ticks), 1 tick remaining
(time-advance: 2) // → health 70 (1 tick fires, status expires, tick 2 does not fire)
// Healing elixir at health 50, +5/tick, duration 6
(time-advance: 4) // → health 70 (4 ticks)
(status-remove: "healing_elixir") // health stays at 70Dice Rolling
A probabilistic resolution system for skill checks and random outcomes. Rolls produce raw integers that can be modified by stat values. Use (check:) for simple threshold checks (see Skills); use dice when you want to show the roll or introduce variance.
(dice: N)
Expression macro. Rolls a random integer from 1 to N (inclusive). N must be a positive integer ≥ 1.
ana
(set: _roll to (dice: 20))
You rolled a _roll.(dice: N, $stat)
Expression macro. Rolls 1–N and adds the computed modifier for the given stat variable. Returns the total (integer). The modifier is derived from the stat's registered bounds (set via (declare:)) and the current modifier scale set by (dice-range:).
ana
(set: _result to (dice: 20, $skill.persuasion))
You rolled _result.If the stat has no registered bounds, the modifier is 0 and only the raw roll is returned.
(dice: N, $stat, threshold)
Expression macro. Rolls and applies the modifier as above, then returns true if the total is ≥ threshold, false otherwise. Use this directly inside (if:) for clean pass/fail resolution.
ana
(if: (dice: 20, $skill.persuasion, 15))[
He softens. "All right, you've convinced me."
](else:)[
He shakes his head. "Not my problem."
]Or capture the roll separately to display it:
ana
(set: _roll to (dice: 20, $skill.persuasion))
(set: _pass to _roll >= 15)
You rolled _roll.
(if: _pass)[
Success — he relents.
](else:)[
Failure — he turns away.
](dice-mod: $stat)
Expression macro. Returns only the modifier integer for a stat, without rolling. Useful for displaying the modifier in a UI before committing to a roll.
ana
Persuasion modifier: (dice-mod: $skill.persuasion)Returns 0 if the stat has no registered bounds.
(dice-range: modMin, modMax) (GameInit only)
Sets the global modifier scale: the range that stat values are mapped onto when computing roll modifiers. Call in GameInit. Defaults to −5 to +5.
ana
(dice-range: -5, 5)
(dice-range: -10, 10) // higher-stakes modifier rangeModifier formula
The modifier is calculated using linear interpolation of the stat's current value within its registered [statMin, statMax] bounds against the configured [modMin, modMax] modifier range:
modifier = round( ((value − statMin) / (statMax − statMin)) × (modMax − modMin) + modMin )Example. $skill.persuasion defined at 0–100, current value 75, modifier scale −5 to +5:
modifier = round( (75 / 100) × 10 − 5 )
= round( 7.5 − 5 )
= round( 2.5 )
= 3So (dice: 20, $skill.persuasion) produces a result in the range 4–23 (1–20 raw + 3 modifier).
At the extremes:
- Value = 0 (statMin) → modifier = modMin (−5 by default)
- Value = 100 (statMax) → modifier = modMax (+5 by default)
- Value = 50 (midpoint) → modifier = 0
Use (dice-range:) in GameInit to scale modifiers to match your game's difficulty expectations. A tighter range (−2 to +2) makes skill less decisive; a wider range (−10 to +10) makes it dominant.