YAML Source is the Network Storage authoring standard.
Use the s&box Library Manager install for auto-updates. GitHub is best for agents, source review, contributions, and manual installs.

Math Expressions

Transform steps support safe math expressions for computing derived values server-side.

# Math Expressions

Transform steps support safe math expressions for computing derived values server-side.

## Syntax

Use the `expression` field instead of `value` on a transform step:

```yml
id: player_level
type: transform
expression: floor({{player.xp}} / {{values.progression.xp_per_level}})
```

All `{{...}}` template variables are resolved first, then the resulting numeric expression is evaluated using a safe parser (no eval). Dynamic path segments are supported inside templates:

```yml
id: fish_value
type: transform
expression: "{{player.inventory.{{input.fishIndex}}.value}}"
```

The inner `{{input.fishIndex}}` resolves first, then the runner reads that inventory row.

## Operators

| Operator | Example | Result |
|----------|---------|--------|
| `+` | `10 + 5` | `15` |
| `-` | `10 - 3` | `7` |
| `*` | `4 * 5` | `20` |
| `/` | `10 / 3` | `3.333...` |
| `%` | `2750 % 1000` | `750` (modulus/remainder) |

Operator precedence follows standard math: `*`, `/`, `%` before `+`, `-`. Use parentheses to override.

## Functions

| Function | Description | Example | Result |
|----------|-------------|---------|--------|
| `floor(x)` | Round down | `floor(3.7)` | `3` |
| `ceil(x)` | Round up | `ceil(3.2)` | `4` |
| `round(x)` | Round to nearest | `round(3.5)` | `4` |
| `min(a, b, ...)` | Smallest value (2+ args) | `min(5, 3)` | `3` |
| `max(a, b, ...)` | Largest value (2+ args) | `max(5, 3)` | `5` |
| `sum(collection, path)` | Sum array/object items, optionally by field path | `sum({{player.inventory}}, Value)` | Total item value |
| `avg(collection, path)` | Average array/object items, optionally by field path | `avg({{fish}}, WeightKg)` | Average weight |
| `count(collection)` | Count array items or object values | `count({{player.inventory}})` | Inventory count |
| `count(collection, predicate)` | Count matching items using a lambda | `count({{player.inventory}}, item => item.sold == false)` | Unsold count |
| `any(collection, predicate)` | True when at least one item matches | `any({{quests}}, item => item.done == true)` | `true`/`false` |
| `all(collection, predicate)` | True when every item matches | `all({{quests}}, item => item.done == true)` | `true`/`false` |
| `first(collection, predicate)` | First item, optionally matching a predicate | `first({{inventory}}, item => item.type == "rod")` | Object/null |
| `find(collection, predicate)` | Alias for first matching item | `find({{inventory}}, item => item.id == "rod_01")` | Object/null |
| `pluck(collection, path)` | Select a field from each item | `pluck({{inventory}}, Value)` | Array |
| `abs(x)` | Absolute value | `abs(-5)` | `5` |
| `random(min, max)` | Random integer (inclusive) | `random(1, 10)` | `1`--`10` |
| `pow(base, exp)` | Exponentiation | `pow(2, 3)` | `8` |
| `clamp(val, min, max)` | Clamp value to range | `clamp(15, 0, 10)` | `10` |
| `now()` | Current Unix timestamp (ms) | `now()` | `1711612800000` |

Functions can be nested: `floor(max(0, {{a}} - {{b}}))`

Aggregate functions accept arrays or objects. When you pass a field path, each item is read by that path and missing values count as `0`; non-numeric selected values fail loudly instead of being silently ignored. Use the implicit `item` name or your own lambda parameter for predicates and computed selectors:

```yml
# Array of fish objects: [{ Value: 10 }, { Value: 25 }]
inventory_value: "sum({{player.inventory}}, Value)"

# Object/map of bins: { raw: { totalValue: 20 }, cooked: { totalValue: 7 } }
bin_value: "sum({{player.bins}}, totalValue)"

# Predicate count / validation helpers
unsold_count: "count({{player.inventory}}, item => item.sold == false)"
has_trophy: "any({{player.inventory}}, item => item.rarity == \"trophy\")"
all_claimed: "all({{player.dailyRewards}}, item => item.claimed == true)"

# Pull values or one matching object for response payloads / later checks
values: { value: "{{pluck(player.inventory, Value)}}" }
first_rod: { value: "{{first(player.inventory, item => item.type == \"rod\")}}" }
```

## Common Patterns

### Compute Level from XP

Store XP in the collection, compute level on the fly:

```yml
id: level
type: transform
expression: floor({{player.xp}} / {{values.progression.xp_per_level}})
```

With `xp = 2750` and `xp_per_level = 1000`: level = 2

### XP Remaining to Next Level

```yml
id: xp_remaining
type: transform
expression: "{{values.progression.xp_per_level}} - ({{player.xp}} % {{values.progression.xp_per_level}})"
```

With `xp = 2750` and `xp_per_level = 1000`: remaining = 250

### XP Progress Percentage

```yml
id: progress_pct
type: transform
expression: floor(({{player.xp}} % {{values.progression.xp_per_level}}) * 100 / {{values.progression.xp_per_level}})
```

With `xp = 2750` and `xp_per_level = 1000`: progress = 75%

### Tax Calculation (5% with minimum of 1)

```yml
id: tax
type: transform
expression: max(1, floor({{item.cost}} * 0.05))
```

### Sum Inventory Value

Use `sum()` instead of hard-coding every possible inventory index:

```yml
id: inventory_totals
type: compute
values:
total_value: "sum({{player.inventory}}, Value)"
unsold_count: "count({{player.inventory}}, item => item.sold == false)"
has_legendary: "any({{player.inventory}}, item => item.rarity == \"legendary\")"
```

This works for both arrays and object maps.

### VIP Discount (20% off)

```yml
id: discounted
type: transform
expression: floor({{item.cost}} * (1 - {{values.shop.vip_discount}}))
```

### Streak Bonus (capped multiplier)

```yml
id: reward
type: transform
expression: floor({{values.daily.base_reward}} * min({{values.daily.max_multiplier}},
1 + {{player.login_streak}} * {{values.daily.streak_multiplier}}))
```

### K/D Ratio (prevent division by zero)

```yml
id: kd_ratio
type: transform
expression: round({{player.stats.kills}} * 100 / max(1, {{player.stats.deaths}}))
/ 100
```

### ELO Rating Change

```yml
id: elo_change
type: transform
expression: round(32 * ({{match_result}} - {{expected_score}}))
```

### Random Loot Amount

```yml
id: loot_amount
type: transform
expression: random({{values.loot.min_drop}}, {{values.loot.max_drop}})
```

### Exponential XP Curve

```yml
id: xp_for_next
type: transform
expression: floor(pow({{level}} + 1, 2) * {{values.progression.xp_base}})
```

### Clamped Damage (bounded range)

```yml
id: final_damage
type: transform
expression: clamp({{raw_damage}} - {{target.armor}}, {{values.combat.min_damage}},
{{values.combat.max_damage}})
```

### Time-Based Cooldown Check

```yml
id: time_since_last
type: transform
expression: now() - {{player.lastClaimTimestamp}}
```

## Using Computed Values

Transform results are stored in the context and available to all subsequent steps:

```yml
- id: level
type: transform
expression: floor({{player.xp}} / {{values.progression.xp_per_level}})
- id: check_level
type: condition
check:
field: level
op: '>='
value: "{{node.xp_required}}"
```

## Expression Checks

`assert` and `condition` checks can use `check.expression` when the predicate is just a math expression plus an optional comparison. This removes one-off transform steps used only for validation.

```yml
- id: rod_owned
type: assert
check:
expression: "max(0, {{num(player.itemInventory.{{rod.id}}, 0)}}) > 0"
status: 409
errorCode: EQUIPPED_ROD_NOT_OWNED
```

Comparison operators supported in expression checks are `>`, `<`, `>=`, `<=`, `==`, and `!=`. If no comparison appears, the numeric result is treated as truthy: non-zero passes, zero fails.

You can also compute the right-hand side of a regular field comparison:

```yml
check:
field: player.gold
op: ">="
expression: "{{input.quantity}} * {{item.price}}"
```

## Returning Computed Values

Include computed values in the response so the game client can display them:

```yml
response:
status: 200
body:
xp: "{{player.xp}}"
level: "{{level}}"
xp_remaining: "{{xp_remaining}}"
progress_pct: "{{progress_pct}}"
```

## Security

- Only numbers, operators, functions, parentheses, commas, and whitespace are allowed
- No string operations, variable assignment, or arbitrary code
- Maximum 1000 characters after template resolution
- Most `{{...}}` variables in math must resolve to numeric values; aggregate inputs like `sum({{player.inventory}}, Value)` may resolve to arrays or objects
- Unresolved variables throw an error (not silently treated as 0)