Storylet Studio

Writing Conditions and Outcome Values

Storylet Studio gives you two slightly different surfaces for the two cases:

  • Conditions (storylet, deck, zone, site) use a tree editor that renders the expression as a structural outline of clauses and groups. This is the right shape for boolean logic.
  • Outcome values (the right-hand side of a property change) use the pill editor — a single horizontal strip of colour-coded pills. Outcome values aren't boolean trees, they're single expressions, so the pill strip suits them better. Adding a new property change runs a small wizard (target → value, with the right input shape per type) so the row arrives ready to edit.

This page covers both.


Conditions: the tree editor

What you see

A condition is laid out as an outline of rows:

ALL of these:
        @season is autumn                  [NOT ↑ ↓ ✕]
   AND  @reputation > 3                    [NOT ↑ ↓ ✕]
   AND  ANY of these:                 [↑ ↓ ✕]
              site_has_tag (indoors)       [NOT ↑ ↓ ✕]
         OR   count_played_tag (npc) > 2   [NOT ↑ ↓ ✕]
         + Add condition
   AND  @site.searched is true             [NOT ↑ ↓ ✕]
   + Add condition

Each row is one clause - a comparison, a function call, or a nested group. The container header ("ALL of these" or "ANY of these") tells you how the clauses combine. Indentation shows nesting.

The connector word at the start of each row (AND or OR) repeats the parent group's operator so the chain reads top-to-bottom like prose. The first row in any group has no connector - the group header already tells you what would go there.

You never need to think about operator precedence. What you see is what the condition does.

Reading the pills inside a row

Each row is built from colour-coded pills:

Each pill's colour matches the type badge it would carry on the All Properties page, so types read consistently across the app:

Pill colour What it means Example
Violet A property reference - world (@season) or scoped (@zone.weather) season, zone.weather
Yellow A text/string value "autumn"
Blue A number value (whole or decimal — same type) 5, 0.5
Cyan A function call (site_has_tag(), count_played_tag(), etc.) site_has_tag
Lime A boolean (true / false) true
Pink A tag name (in site_has_tag() / count_played_tag() / rounds_since_tag()) - distinct from a plain string indoors
Emerald / Rose A flag pill inside check_flags() / set_flags() - emerald for set (+), rose for unset (−) +met_alice, −tree_task
Red box An invalid pill - the property has been deleted, the flag does not exist, the value type does not match ~~ghost_property~~

Operators within a row are written between pills: is, , >, , <, , +, , etc. NOT is shown in bold red when it appears.

A small red dot at the end of a row indicates a validation error. Hover for the message, or open the row to see per-pill detail.

Editing a row

Click any pill inside a row. A small popover opens with the right kind of editor:

  • Property pill - a searchable picker grouped by scope (World / Deck / Zone / Site). Below the picker, Use a literal value instead… lets you swap the property for a typed-in value (true/false, a number, a text or enum value).
  • Text / number / boolean pill - the right input. Text values being compared against an enum get a dropdown of valid values instead of free text. Below the input, Use a property instead… swaps the literal for a property reference of compatible type.
  • Flag pill - opens the Flags Set? popup with a +set / −unset toggle and a dropdown of valid flag names from the parent flags property.
  • Operator pill (is, >, etc.) - a dropdown of operators in the same group (comparisons swap with comparisons, arithmetic swaps with arithmetic).
  • Function pill (cyan) - the popover here is a structural menu: NOT to negate the function, Delete to remove the whole call.

The tag-name slots inside site_has_tag(), count_played_tag(), and rounds_since_tag() are role-aware - clicking them re-opens the same tag input you saw when you first inserted the clause, so re-edits stay structured rather than dropping you into raw text. Flag pills inside check_flags() / set_flags() open a sign toggle plus a list of valid flag names from the parent flags property.

Every value popover also has a structural footer with NOT (negate this term) and Delete (remove this term).

Adding clauses and groups

At the bottom of every container you'll see two buttons side by side: + Add condition and + Add group. The first opens an insertion popover with the available clause templates; the second is a direct shortcut that drops in a nested sub-group with the OPPOSITE operator (so inside an AND container "Add group" creates an OR, and vice versa - same-operator nesting just flattens away, so it would be a no-op).

The clause templates available behind + Add condition (in menu order — flag checks come first since they're the most common gate):

  • check_flags() - check one or more flags on a flags property (each +flag must be set, each -flag must be unset)
  • Property comparison - @property is value
  • Property is true - a bare boolean check, just @boolean_property with no operator
  • site_has_tag() - does the site this storylet is running on carry this tag? (Tag the site once on its Tags tab in the Plot editor; gate any number of world- or zone-scoped storylets on the same tag.)
  • count_played_tag() - how many storylets with this tag have played?
  • rounds_since_tag() - cooldown since the last play with this tag
  • random(a, b) - pick a random whole number from a to b inclusive (e.g. dice rolls or % chance)

Every template runs a short wizard so the new clause arrives ready to use:

  • Property comparison asks for the left-hand property, then the operator (filtered by type - numeric properties offer > / / < / ; text/enum/boolean only get is / ), then the right-hand value. The right-hand step has a Use a property instead… option so you can compare against another property rather than a typed-in value.
  • Property is true is a one-step shortcut for boolean properties: pick the property and you're done. The clause is just @property with no operator. Use the row's NOT toggle if you want "is false".
  • check_flags() asks you to pick a flags property, then a sign (+set / −unset) and the first flag name from its declared values. The clause renders compactly as ⚑ <prop> ( +flag, −flag ) — the leading ⚑ glyph is the function pill (click it for delete / wrap-in-NOT). Add more flag pills inline from the call's + chip after it lands in the row.
  • site_has_tag() asks you to type a tag name (the same string you'd add on the site's Tags tab).
  • count_played_tag() / rounds_since_tag() ask for the tag, then a comparison operator (>, , ==, , <, ), then the value to compare against. Any threshold or equality test works - no hard-coded > 0 / ≥ 1 to chase down afterwards.
  • random(a, b) asks for a range (the low and high bounds, both whole numbers), then a comparison operator, then the value to compare against. The runtime picks a random whole number from a to b inclusive — so random(1, 6) is a six-sided die, and random(1, 100) <= 30 fires roughly 30% of the time.

The "+ Add group" button is labelled by the operator it'll create: + Add OR group inside an AND container, + Add AND group inside an OR container. (The new sub-group always uses the OPPOSITE operator from its parent - mixing AND with OR is the only nesting that introduces real precedence; same-op nesting just flattens away.) Clicking it opens the same template menu as +Add condition; the clause you pick becomes the first clause of the new sub-group. The sub-group's second slot starts as a dashed + Click to add condition placeholder; click it (when you're ready) to add the second clause via the same menu.

Deleting clauses inside a sub-group:

  • The placeholder slot's is disabled — you can't delete an empty slot on its own. Add a clause to it (or delete its real sibling) to dismiss the slot.
  • If one clause is still a placeholder and you delete the real one, the whole sub-group is thrown away (the placeholder doesn't survive on its own).
  • If both clauses are real and you delete one, the deleted side becomes a fresh placeholder; the sub-group stays in place with one real clause + one empty slot. This way you can't accidentally lose the group you intentionally created — to dissolve it, delete the second real clause too (which lands you in the "real + placeholder" case above) or delete the sub-group as a whole from the parent's row actions.

Reordering and removing clauses

Each clause row has a NOT ↑ ↓ ✕ stack on the right:

  • NOT negates this single row. When active, the button fills red and a bold red NOT appears at the start of the row content. Click again to remove the negation.
  • moves the row up within its container (disabled at the top)
  • moves the row down (disabled at the bottom)
  • removes the row

Removing one side of a two-clause container leaves the surviving clause as the row in its place. Removing it again clears the whole condition.

Negating a group

Each container header has a NOT toggle next to the operator label. Click it to wrap or unwrap the whole group:

  • AND container with NOT off → "ALL of these"
  • AND container with NOT on → bold red NOT button next to "ALL of these" - the group as a whole is negated

You can also negate individual rows from their value popover footer.

Switching ALL ↔ ANY

The outermost group's "ALL of these" / "ANY of these" header is clickable - click it to flip the whole condition between AND and OR. The flip is immediate; click again to flip back.

Nested sub-groups don't have a flip button. They show a static label only. The reason: a sub-group is always inserted with the OPPOSITE operator from its parent (mixing AND with OR is the only nesting that's grammatically meaningful), so the only flip a sub-group could perform would dissolve it into the parent chain - and that's almost never what you want. To restructure a sub-group, delete its contents and use + Add OR group / + Add AND group to rebuild it the way you want.

Empty conditions

When a condition is empty, you'll see the [always] placeholder pill alongside an "Add your first condition" button. Click it to seed the expression — the same template menu as above.

An empty condition is treated as "always true" by the runtime, so leaving conditions empty is a valid choice.


Outcome property changes: the pill editor

Property changes use the pill-strip editor — outcome values are single expressions (a value, not a boolean tree), so the strip suits them better.

Adding a property change

The + Add property change button opens a small two-step wizard:

  1. Pick the target property — searchable list grouped by scope (World / Deck / Zone / Site).
  2. Pick the initial value, with the input shape matching the target's type:
    • Booleantrue / false buttons
    • Number → number input (integer or decimal)
    • Text → free-text input
    • Enum → list of declared values to choose from
    • Flags → a toggle grid of every declared flag — click to cycle each one between no change → + (set) → − (clear), then Add when at least one flag is selected. Stored as set_flags(@target, +flagA, -flagB) under the hood, but you don't need to think about that.

The wizard always starts in structured mode — there's no raw-text textarea staring at you on insert. The row appears with both the target and value populated as pills.

Editing an existing property change

An outcome row must always carry at least one term — the last value pill can't be deleted, only edited. (For string targets you can clear the value to empty by editing the pill: it stays in the row as the _empty_ placeholder you also see in the condition editor.) For flags rows the same rule applies to flag-delta pills: the last +name / −name can't be removed, only changed.

For number targets the row has a small + term button after the value pills. Click it to extend the expression with another arithmetic term — pick an operator (+ / / × / ÷) and either a number or another number-typed property. Repeat to build up chains like @count + 1, @a + @b * 2, or @hp − @damage. The wizard refuses to commit ÷ 0; if you slip one in via raw text, the row's red error pill flags it.

For boolean targets the same + term button appears, but with AND / OR operators and a true / false value step (or a boolean property pick). Use it to build chains like @flag_a or @flag_b, @is_summer and @rain_today.

(String / enum / flags targets don't get the term button — those values are single.)

Click any pill to edit it (same as in the condition editor):

  • Property pill (target or value) - swap to a different property of the same type, or "Use a literal value instead…" to swap to a literal.
  • Text / number / boolean pill - type a new value, or "Use a property instead…" to swap to a property reference.
  • Enum pill - dropdown of valid enum values.
  • Flag pill (+name / −name) - sign toggle, a flag-name list, and Delete. For flags targets the row hides the implied set_flags(@target, …) wrapper and shows just the flag-delta pills, plus an inline + chip to add another flag (no duplicates - flags already used in this change are filtered out). The wrapper is still what's stored on disk; you just don't have to look at it.
  • Function pill (cyan) - structural menu (Delete).

Use the corner </> toggle to drop to raw text when you need to paste or type something complex.


Raw text: the escape hatch

Both surfaces have a small </> icon in the corner. Click it to swap the structured editor for a textarea showing the canonical raw expression text - the structured view is hidden while you're in raw mode. Click </> again to swap back.

The structured editors handle every common authoring action. Drop to raw text when:

  • You're pasting an expression from elsewhere
  • You're typing a complex arithmetic expression like @score * 2 + 5
  • The structured editor's fallback shape ("wrapped expression" rows) appears for something the tree does not yet model

Anything you type in raw text round-trips back to pills (or tree rows) as soon as it parses.


Side by side: condition tree and raw text

Tree Raw text
ALL of these: • @season is autumn • @reputation > 3 @season == autumn and @reputation > 3
NOT • ALL of these: • @season is autumn not (@season == autumn)
ALL (AND): • @x is true • ANY (OR): • site_has_tag(a) • site_has_tag(b) @x == true and (site_has_tag(a) or site_has_tag(b))

The tree always preserves the meaning of the expression; only the text representation differs.


Tips

  • The [always] placeholder appears whenever a condition is empty. It is not an error - it just says "this storylet has no condition gating it".
  • A red dot in the top-right of any field means there's a validation error somewhere. The errors also appear inline below the field, and the storyworld-level error banner at the bottom of the page lists everything.
  • Outcome values for flags properties get a toggle grid the first time you set them up (during the +Add property change wizard); after that they live as pills in the row, just like every other type, and you edit them by clicking individual flag pills or the inline + chip.