Skip to content

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:

VariableUse for
$player.eventsThings that happened involving the player
$npc.bartender.eventsThings that happened with a specific NPC
$world.eventsMajor story beats that persist forever
$world.todayEventsEvents 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):

TagEvent firedSide effect
locationlocationEnterSets $world.location to the passage name
chapterchapterStartSets $world.chapter to the passage name
shopshopEnterNone
endingendingReachedNone

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

EventFired whenContext vars
gameStartNew game begins (after @system(init) runs)None
gameSaveGame saved successfully_slot
locationEnterNavigation to a [location] passageNone
chapterStartNavigation to a [chapter] passageNone
shopEnterNavigation to a [shop] passageNone
endingReachedNavigation to an [ending] passageNone
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
equipChangeItem equipped or unequipped via (add:)/(remove:)_slot, _itemId
statusApply(status-apply:) activates a status_statusId
statusExpireA 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
dayAdvanceDay count rolls over in (time-advance:)None
periodChangeTime 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

KwargBehaviorApplyRemove / Expiry
effectFlat buff/debuff; stat changes while activeDelta applied immediatelyDelta reversed
dotRecurring effect; delta applied each (time-advance:) tickNo immediate changeNothing reversed; stat stays wherever ticks left it
(none)Flag only, no stat changeNoneNone

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 messages
  • description, "...": tooltip or description text
  • effect, $var, delta: flat modifier, applied on activate, reversed on remove/expiry
  • dot, $var, delta: recurring modifier, applied each tick, never reversed
  • duration, N: auto-removes after N (time-advance:) steps; omit or -1 for indefinite
  • duplicate, mode: behavior when (status-apply:) is called while already active: ignore (default), reset, extend-full, or extend-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.

ModeBehavior
ignoreNo-op; status is unchanged. "You can't be poisoned twice."
resetRemaining duration resets to the full defined value.
extend-fullFull defined duration added to remaining duration (additive stacking).
extend-halfHalf 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 -1 if the status is active but indefinite (no duration set, or duration, -1).
  • Returns 0 if 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 70

Dice 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 range

Modifier 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 )
         = 3

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