> For the complete documentation index, see [llms.txt](https://series-1.gitbook.io/rundot-docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://series-1.gitbook.io/rundot-docs/v5.21.0/readme/simulation_config.md).

# Simulation: Config Reference (BETA)

Define your game's server-authoritative state, actions, and randomized rewards entirely through JSON configuration. This reference covers the full config schema: entities, recipes, loot tables, and lifecycle hooks.

> This page is the config-authoring companion to the [Simulation API](/rundot-docs/v5.21.0/readme/server_authoritative.md), which documents the client SDK methods. If you're looking for a specific pattern (energy systems, gacha, timers), see the recipe docs first; they include complete configs inline.

***

## Project Setup

Simulation config is uploaded to the server when you `rundot deploy`. Place it in the `.rundot/simulation/` directory.

### Recommended: `.rundot/simulation/` directory

```
my-game/
├── .rundot/
│   └── simulation/
│       ├── entities.json
│       ├── recipes.json
│       ├── loot-tables.json
│       └── lifecycle.json
├── src/
├── dist/
├── game.config.prod.json        ← game ID + build settings (separate file)
└── package.json
```

Each `.json` file holds raw simulation content: **no `{"simulation": {...}}` wrapper** (the directory implies it). All files are deep-merged alphabetically at deploy time with collision detection: two files defining the same leaf value differently is an error (identical values are fine). Split however makes sense for your project:

```
.rundot/simulation/
├── entities.json              ← all entities
├── crafting-recipes.json      ← one file per recipe group
├── combat-recipes.json
├── loot-tables.json
└── lifecycle.json
```

Example `entities.json`:

```json
{
  "entities": {
    "gold": {
      "tags": ["currency"],
      "clientViewable": true,
      "metadata": { "displayName": "Gold" }
    },
    "energy": {
      "tags": ["resource"],
      "clientViewable": true,
      "metadata": { "displayName": "Energy" }
    }
  }
}
```

Every entity must include `metadata.displayName` (the deploy-time validator rejects entities without it). There is no `type` or `defaultValue` field: entities are plain numeric quantities, and starting values come from an `onStart` lifecycle recipe (see [Lifecycle](#lifecycle)), not a `defaultValue`.

Other server systems each get their own file alongside `simulation/`: `.rundot/leaderboard.config.json`, `.rundot/shop.config.json`, `.rundot/ugc.config.json`, etc. (each contains its value directly, no wrapping key). See each system's doc for details.

> **Commit `.rundot/`** to your repo; it's project config like `package.json`, not a build artifact. The folder is env-agnostic: it applies to every environment and takes priority over legacy `config.{local,staging}.json` for any system it defines (the sandbox prints a warning when an env-specific override is shadowed this way).

### Also supported: legacy layouts

Legacy `config.json` / `config/` layouts keep working indefinitely. To move an existing project into `.rundot/` automatically, run `rundot migrate-config` (add `--dry-run` to preview).

<details>

<summary>Single <code>config.json</code> (simple games)</summary>

Add a `simulation` key to your project's `config.json`:

```json
{
  "simulation": {
    "entities": { ... },
    "recipes": { ... }
  }
}
```

</details>

<details>

<summary><code>config/</code> directory (split files)</summary>

Split across `config/*.config.json` files. Each file **must** use the `{"simulation": {...}}` wrapper (unlike `.rundot/simulation/`, where the directory implies it):

```
my-game/
├── config/
│   ├── energy-system.config.json
│   ├── combat.config.json
│   └── store.config.json
├── src/
└── dist/
```

</details>

### Important

* **`.rundot/`** (or legacy `config.json` / `config/`) holds server config (simulation, leaderboard, rooms, etc.)
* **`game.config.prod.json`** is a separate file for local CLI metadata only (`gameId`, `relativePathToDistFolder`, `usesPreloader`); simulation config does not go there

***

## Config Structure

Each config file's `simulation` object can contain any combination of these top-level keys:

```json
{
  "simulation": {
    "version": "1.0.0",
    "lifecycle": { ... },
    "entities": { ... },
    "recipes": { ... },
    "lootTables": { ... },
    "clientConfig": { ... },
    "formulaPools": { ... },
    "fieldResolution": { ... }
  }
}
```

Files are merged by key: if two files both define `entities`, their entities are combined. If two files define the same entity ID, the last file wins.

| Key                             | Description                                                                                                                                                                       |
| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `version`                       | Config version string. Required by the validator. The `.rundot/simulation/` deploy pipeline injects it for you; in the legacy single-`config.json` layout you supply it yourself. |
| `lifecycle`                     | Hooks that run at lifecycle points (`onStart`, `onReset`). See [Lifecycle](#lifecycle).                                                                                           |
| `entities`                      | Game-state quantities. See [Entities](#entities).                                                                                                                                 |
| `recipes`                       | Server-authoritative actions. See [Recipes](#recipes).                                                                                                                            |
| `lootTables`                    | Randomized reward pools. See [Loot Tables](#loot-tables).                                                                                                                         |
| `clientConfig`                  | Free-form game constants passed through to the client unchanged (reward thresholds, tuning values, etc.).                                                                         |
| `formulaPools`                  | Named weighted pools (`entries: [{ value, weight }]`, optional `noRepeatWindow`) that formula functions can draw from (quest rotation, weighted selection).                       |
| `fieldResolution.slotTemplates` | Reusable slot definitions referenced by an entity's `slotsTemplate`. Also holds `slotAggregations`, `equipmentMappings`, and `patternMappings` for derived field math.            |

***

## Entities

Entities are the atomic units of game state. Each entity tracks a numeric quantity per player (e.g., `gold: 150`, `energy_current: 12`, `building_barracks: 3`).

```json
{
  "simulation": {
    "entities": {
      "currency_coins": {
        "tags": ["currency"],
        "clientViewable": true,
        "metadata": {
          "displayName": "Coins",
          "iconComponent": "coin",
          "color": "#FFD700"
        }
      },
      "energy_current": {
        "tags": ["resource", "energy"],
        "clientViewable": true,
        "metadata": {
          "displayName": "Energy",
          "description": "Used to start battles. Regenerates over time.",
          "maxAmount": 20,
          "regenIntervalMs": 600000
        }
      }
    }
  }
}
```

### Entity Fields

| Field                      | Type       | Required | Description                                                                                                                                                          |
| -------------------------- | ---------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `metadata.displayName`     | `string`   | **Yes**  | Human-readable name. Deploy fails with a validation error if any entity omits it.                                                                                    |
| `tags`                     | `string[]` | No       | Categorize entities for filtering, subscriptions, and loot table lookups. Use any string.                                                                            |
| `clientViewable`           | `boolean`  | No       | Whether the client can read this entity's quantity via `getStateAsync` and subscriptions. Default `false`.                                                           |
| `neverConsumable`          | `boolean`  | No       | If `true`, recipe inputs can check this entity but never deduct from it. Useful for cumulative counters and permanent unlocks.                                       |
| `requiresManualCollection` | `boolean`  | No       | If `true`, timed recipe outputs for this entity require explicit collection via `collectRecipeAsync`.                                                                |
| `slots`                    | `object`   | No       | Slot-container schema. Maps slot IDs to `{ allowedTags, maxItems, requirements?, effects? }`. See [Slot Containers](#slot-containers).                               |
| `slotsTemplate`            | `string`   | No       | Reference to a reusable slot definition in `fieldResolution.slotTemplates` instead of inlining `slots`.                                                              |
| `tracking`                 | `object`   | No       | Automatic delta tracking: `{ onDecrease?, onIncrease? }`. See [Delta Tracking](#delta-tracking).                                                                     |
| `rateAccumulator`          | `object`   | No       | Rate-based accumulation for idle/income entities. See [Rate Accumulators](#rate-accumulators).                                                                       |
| `actorTemplate`            | `object`   | No       | Defaults for the actor system: `{ defaultState?, availableRecipes?, spawnLocation? }`. Used by games with room/actor-scoped recipes and `create_actor` effects.      |
| `metadata`                 | `object`   | Yes      | Key-value data readable by both config formulas and client code (display names, icons, costs, thresholds). Must contain `displayName`; everything else is free-form. |

Quantities are plain numbers with no per-entity stack cap. If you need to cap an amount, enforce it with a recipe guard, a formula, or a `metadata.maxAmount` value your own logic reads. (The SDK type loosely declares an entity `stackable` flag, but the engine ignores it: it has no effect.)

#### Slot Containers

Container entities hold assignable items in named slots. Power-team and equipment systems use this; it backs the `assign_to_slot` effect and the slot SDK methods (`getSlotContainersAsync`, `getAvailableItemsAsync`, `validateSlotAssignmentAsync`).

```json
"deck_1": {
  "clientViewable": true,
  "metadata": { "displayName": "Battle Deck" },
  "slots": {
    "unit_1": { "allowedTags": ["unit"], "maxItems": 1 },
    "unit_2": { "allowedTags": ["unit"], "maxItems": 1, "requirements": ["building_barracks"] }
  }
}
```

| Slot Field     | Type       | Description                                                        |
| -------------- | ---------- | ------------------------------------------------------------------ |
| `allowedTags`  | `string[]` | Only items carrying one of these tags can be assigned.             |
| `maxItems`     | `number`   | Maximum items the slot holds.                                      |
| `requirements` | `string[]` | Entity IDs that must be present before the slot unlocks.           |
| `effects`      | `array`    | Effects applied when an item is assigned (e.g., stat aggregation). |

Set `slotsTemplate: "<templateId>"` to reuse a slot layout defined under `fieldResolution.slotTemplates` instead of inlining `slots`.

#### Delta Tracking

`tracking` mirrors quantity changes into a separate counter entity, useful for cumulative spend/earn counters that unlock features at thresholds (e.g., "spend 10 gems to unlock ad skip").

```json
"currency_gems": {
  "clientViewable": true,
  "metadata": { "displayName": "Gems" },
  "tracking": { "onDecrease": "lifetime_gems_spent" }
}
```

| Field        | Type     | Description                                                                                                 |
| ------------ | -------- | ----------------------------------------------------------------------------------------------------------- |
| `onDecrease` | `string` | When this entity is consumed as a recipe input, the named tracker entity increments by the amount consumed. |
| `onIncrease` | `string` | When this entity is granted as a recipe output, the named tracker entity increments by the amount granted.  |

The tracker entity must exist. Give it `neverConsumable: true` so it never depletes (the validator warns otherwise).

#### Rate Accumulators

`rateAccumulator` is an alternative to auto-restart timers for idle/income entities. The stored value is a checkpoint; the engine computes the live value on demand as `checkpoint + sum(rateEntities) * elapsedSeconds`. A reserved companion field `{entityId}__rate_checkpoint_ms` holds the checkpoint timestamp (you cannot name an entity with that suffix).

```json
"gold": {
  "clientViewable": true,
  "metadata": { "displayName": "Gold" },
  "rateAccumulator": {
    "rateEntities": ["mine_level"],
    "maxOfflinePeriod": 86400,
    "offlineMultiplier": 0.5
  }
}
```

| Field                     | Type               | Description                                                                                                                                                             |
| ------------------------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `rateEntities`            | `string[]`         | Entity IDs whose values are summed to derive the per-second rate. Required and non-empty.                                                                               |
| `maxOfflinePeriod`        | `number \| string` | Max **seconds** of offline accumulation (caps catch-up). A number is literal seconds; a string is an entity ID whose value is used.                                     |
| `offlineMultiplier`       | `number \| string` | Multiplier (0 to 1) applied to income earned while offline. Default `1.0`. A string is an entity ID reference.                                                          |
| `offlineThresholdSeconds` | `number`           | Seconds of offline time before the multiplier kicks in; below it, income accrues at full rate. Default `0`.                                                             |
| `lastGrantDeltaEntity`    | `string`           | Entity to write the last computed offline delta into, so recipes can reference server-computed offline income (e.g., ad-bonus doubling) without trusting client values. |

### Tagging Patterns

Tags enable powerful filtering throughout the system:

```json
"unit_orc_warrior": {
  "tags": ["unit", "collectible", "rarity:common", "faction:orcs"],
  "clientViewable": true,
  "metadata": { "displayName": "Orc Warrior" }
}
```

* **Client subscriptions** filter by tags: `subscribeAsync({ tags: ['currency'] })`
* **Loot tables** select entities by tag: `"includeTags": ["unit", "rarity:epic"]`
* **Recipes** can be tagged for client discovery

***

## Recipes

Recipes are server-authoritative actions. They consume inputs, produce outputs, and optionally run effects. Every game action (spending currency, opening packs, upgrading items, regenerating energy) is a recipe.

### Instant Recipe

Executes immediately. Duration is `0`.

```json
{
  "battle_start": {
    "duration": 0,
    "scope": "player",
    "clientViewable": true,
    "inputs": {
      "energy_current": 5
    },
    "outputs": {
      "battle_pending": 1
    },
    "metadata": {
      "displayName": "Start Battle"
    }
  }
}
```

```typescript
import RundotGameAPI from '@series-inc/rundot-game-sdk/api'

await RundotGameAPI.simulation.executeRecipeAsync('battle_start')
```

### Timed Recipe

Has a non-zero `duration` (milliseconds). Creates an active run that must complete before outputs are granted.

```json
{
  "upgrade_barracks": {
    "duration": 300000,
    "scope": "player",
    "clientViewable": true,
    "concurrency": "global:building_upgrade",
    "onConflict": "fail",
    "inputs": {
      "currency_bricks": 200
    },
    "outputs": {
      "building_barracks": 1
    }
  }
}
```

```typescript
const result = await RundotGameAPI.simulation.executeRecipeAsync('upgrade_barracks')
// result.runId: track progress via activeRuns subscription

const runs = await RundotGameAPI.simulation.getActiveRunsAsync()
// When run completes, collect outputs:
await RundotGameAPI.simulation.collectRecipeAsync(runs[0].id)
```

### Auto-Restart Recipe

A timed recipe that automatically re-executes after completing. Ideal for regeneration timers and passive production.

```json
{
  "energy_regeneration": {
    "duration": 600000,
    "autoRestart": true,
    "concurrency": "single",
    "onConflict": "fail",
    "maxOfflineExecutionPeriod": 604800,
    "inputs": {},
    "outputs": {
      "energy_current": 1
    },
    "maxRestartCondition": {
      "entity": "energy_current",
      "maxValue": 20
    },
    "clientViewable": true,
    "scope": "player"
  }
}
```

### Entity-Scoped Recipe

A single recipe definition that operates on different entities at runtime. Uses `{{entity}}` as a placeholder resolved when the recipe is executed.

```json
{
  "upgrade_card": {
    "duration": 0,
    "scope": "player",
    "clientViewable": true,
    "inputs": {
      "currency_coins": {
        "formula": "round(2000 * 1.12 ^ max(0, level - 1) / 10) * 10",
        "variables": {
          "level": { "inventory": "{{entity}}_level" }
        }
      }
    },
    "outputs": {
      "{{entity}}_level": 1
    }
  }
}
```

```typescript
await RundotGameAPI.simulation.executeScopedRecipeAsync(
  'upgrade_card',
  'unit_orc_warrior'
)
```

At execution time, `{{entity}}` resolves to `unit_orc_warrior`, so the recipe reads `unit_orc_warrior_level` and increments it.

### Recipe Fields

| Field                       | Type                                          | Description                                                                                                                                   |
| --------------------------- | --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `duration`                  | `number \| formula`                           | Milliseconds. `0` for instant. Supports formulas for dynamic durations.                                                                       |
| `scope`                     | `"player" \| "room" \| "actor"`               | Execution scope. `player` is the common single-player case; `room`/`actor` power multiplayer/room-shared and per-actor recipes.               |
| `clientViewable`            | `boolean`                                     | Whether the client can see this recipe in config and requirements checks.                                                                     |
| `inputs`                    | `object`                                      | Entities consumed when the recipe executes. See [Inputs](#inputs).                                                                            |
| `outputs`                   | `object`                                      | Entities produced. For loot, use `rolls`. See [Outputs](#outputs).                                                                            |
| `guards`                    | `object`                                      | Preconditions that must pass before execution. See [Guards](#guards).                                                                         |
| `beginEffects`              | `array`                                       | Side effects that run when the recipe starts. See [Effects](#effects).                                                                        |
| `endEffects`                | `array`                                       | Side effects that run when a timed recipe completes.                                                                                          |
| `autoRestart`               | `boolean`                                     | Re-execute automatically after completion.                                                                                                    |
| `autoRestartAtUtcMidnight`  | `boolean`                                     | For auto-restart recipes: re-run aligned to the next UTC midnight instead of immediately. Use for daily resets (daily rewards, daily quests). |
| `concurrency`               | `"single" \| "unlimited" \| number \| string` | Controls parallel execution. See [Concurrency](#concurrency).                                                                                 |
| `onConflict`                | `"fail" \| "queue" \| "replace"`              | What to do when concurrency is violated. See [Concurrency](#concurrency).                                                                     |
| `maxRestartCondition`       | `object`                                      | Stop auto-restart when entity reaches a threshold.                                                                                            |
| `maxOfflineExecutionPeriod` | `number`                                      | Max **seconds** of offline catch-up for auto-restart recipes.                                                                                 |
| `startsDisabled`            | `boolean`                                     | If `true`, the recipe cannot run until enabled via an `enable_recipe` effect.                                                                 |
| `developmentOnly`           | `boolean`                                     | If `true`, the recipe runs only in development mode. Use for debug/cheat recipes that must never run in production.                           |
| `blockedByEntityStates`     | `object`                                      | Map of entity ID to state values that block this recipe (cross-recipe gating).                                                                |
| `blocksRecipes`             | `string[]`                                    | Recipe IDs that cannot run while this recipe is active (mutual exclusion).                                                                    |
| `multiplayer`               | `object`                                      | Competitive/cooperative config for `room`/`actor` recipes. See [Multiplayer Recipes](#multiplayer-recipes).                                   |
| `tags`                      | `string[]`                                    | Categorize recipes for client-side filtering.                                                                                                 |
| `metadata`                  | `object`                                      | Arbitrary data. Use `hidden: true` to suppress from UI.                                                                                       |
| `trigger`                   | `object`                                      | Auto-execute when watched entities change. See [Triggers](#triggers).                                                                         |

***

## Inputs

Recipe inputs define what is consumed when the recipe runs.

### Fixed Amount

```json
"inputs": {
  "energy_current": 5
}
```

### Formula-Based

```json
"inputs": {
  "currency_coins": {
    "formula": "floor(baseCost * costMultiplier ^ level)",
    "variables": {
      "baseCost": { "entity": "{{entity}}", "field": "metadata.baseCost" },
      "costMultiplier": { "entity": "{{entity}}", "field": "metadata.costMultiplier" },
      "level": { "inventory": "{{entity}}" }
    }
  }
}
```

### Wallet (Premium Currency)

```json
"inputs": {
  "wallet": {
    "amount": 10,
    "walletCurrency": "premiumCurrency"
  }
}
```

For dynamic wallet amounts passed at runtime:

```json
"inputs": {
  "wallet": {
    "param": "walletAmount",
    "walletCurrency": "premiumCurrency"
  }
}
```

### Max Guard on Input

Limits how many times an entity can be incremented (useful for one-time or limited purchases):

```json
"inputs": {
  "starter_pack_purchased": { "max": 1 },
  "wallet": { "amount": 10, "walletCurrency": "premiumCurrency" }
}
```

### Advanced Input Options

An input can be an object with these additional fields:

| Field           | Type                | Description                                                                                                                                                                       |
| --------------- | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `amount`        | `number \| formula` | Quantity to consume. Optional for `validateOnly` inputs.                                                                                                                          |
| `min` / `max`   | `number`            | Range bounds for the input amount.                                                                                                                                                |
| `formula`       | `string`            | Computed cost expression returning a number.                                                                                                                                      |
| `validateOnly`  | `boolean`           | Require the entity to be present but do not consume it (skills, tools, gating items).                                                                                             |
| `as`            | `string`            | Store the resolved value in execution context under this name for later effects/formulas.                                                                                         |
| `allowedTags`   | `string[]`          | Accept any entity carrying one of these tags as the input (tag-based input).                                                                                                      |
| `useBigNumbers` | `boolean`           | Use big-number arithmetic for very large quantities.                                                                                                                              |
| `blockedWhen`   | `array`             | Block execution when a status/entity condition holds. Each entry is `{ entity, threshold, operator, message? }` where `operator` is one of `eq`, `gte`, `gt`, `lt`, `lte`, `neq`. |

```json
"inputs": {
  "skill_lockpicking": { "validateOnly": true },
  "currency_coins": { "formula": "100 * level", "as": "coinCost" }
}
```

***

## Outputs

### Fixed Amount

```json
"outputs": {
  "currency_coins": 500,
  "pack_common": 1
}
```

### Formula-Based

```json
"outputs": {
  "currency_bricks": {
    "formula": "min(entities.pending_bricks, entities.brick_storage_max - entities.currency_bricks)"
  }
}
```

### Loot Rolls

For randomized rewards, outputs can contain `rolls` that reference loot tables:

```json
"outputs": {
  "rolls": [
    {
      "table": "common_pack_cards",
      "count": { "min": 3, "max": 5 },
      "guarantees": [
        { "tag": "rarity:rare", "minCount": 1 }
      ],
      "pity": {
        "counterEntity": "pity_counter_mid",
        "threshold": 10,
        "guaranteedTag": "rarity:epic",
        "resetOnHit": true
      }
    },
    {
      "table": "common_pack_coins",
      "count": 1
    }
  ]
}
```

| Roll Field   | Type                     | Description                                                                                                        |
| ------------ | ------------------------ | ------------------------------------------------------------------------------------------------------------------ |
| `table`      | `string`                 | Loot table ID to roll from.                                                                                        |
| `count`      | `number \| { min, max }` | How many times to roll. Fixed or random range.                                                                     |
| `guarantees` | `array`                  | Minimum counts by tag (e.g., guarantee at least 1 rare).                                                           |
| `pity`       | `object`                 | Pity system: increment a counter each roll, guarantee a tag when threshold is hit.                                 |
| `guards`     | `array`                  | Conditional guards gating this roll. The roll only runs when its guards pass (e.g., a bonus roll for VIP players). |

***

## Guards

Guards are preconditions checked before a recipe can execute. If any guard fails, the recipe is rejected.

```json
"guards": {
  "hasPending": {
    "formula": "entities.battle_pending >= 1"
  },
  "notCleared": {
    "formula": "entities.campaign_humans_cleared == 0"
  }
}
```

Guard formulas have access to `entities.*` for current inventory values.

***

## Effects

Effects are side actions that run alongside a recipe. `beginEffects` fire when the recipe starts; `endEffects` fire when a timed recipe completes.

### `set`: Set Entity to Value

```json
{ "type": "set", "entity": "energy_current", "value": 20 }
```

### `add`: Add to Entity

```json
{ "type": "add", "entity": "currency_coins", "value": 500 }
```

Value can be a formula:

```json
{
  "type": "add",
  "entity": "currency_coins",
  "value": {
    "entity": "{{entity}}",
    "field": "metadata.repeatCoinReward",
    "fallback": 0
  }
}
```

### `multiply` / `min` / `max`: Arithmetic on Entity

The same `{ type, entity, value }` shape as `set`/`add`, with a different operation. `value` accepts a number, field reference, or formula.

```json
{ "type": "multiply", "entity": "currency_coins", "value": 2 }
{ "type": "max", "entity": "energy_current", "value": 20 }
{ "type": "min", "entity": "energy_current", "value": 0 }
```

`multiply` scales the current value; `min`/`max` clamp it (`max` caps at the value, `min` floors at it).

### `grant_rewards`: Grant from Metadata

Reads a rewards object from an entity's metadata and grants it. Defaults to the scoped entity if `entity` is omitted.

```json
{
  "type": "grant_rewards",
  "source": { "entity": "{{entity}}", "field": "metadata.repeatRewards" }
}
```

### `enable_recipe`: Allow Future Execution

Re-enables a recipe (the inverse of `disable_recipe`). Pairs with `startsDisabled` to gate recipes until unlocked.

```json
{ "type": "enable_recipe", "selector": "advanced_crafting" }
```

### `compute`: Precompute a Value

Evaluates a formula or rolls a weighted table and stores the result in context under `as`, so later effects in the same recipe can reference `results.<as>`. Provide exactly one of `formula` or `weightedTable`.

```json
{
  "type": "compute",
  "weightedTable": [
    { "value": "rare", "weight": 70 },
    { "value": "epic", "weight": 30 }
  ],
  "as": "tier"
}
```

### `reduce_run_expiry`: Speed Up a Timer

Reduces an active timed run's remaining time. Use for speedups that consume tokens/gems/ad credits. `target` is a concurrency group string (e.g., `"global:building_upgrade"`) or `{ param: "<inputKey>" }` to target a specific run ID.

```json
{
  "type": "reduce_run_expiry",
  "target": "global:building_upgrade",
  "reductionMs": 60000,
  "minRemainingMs": 0
}
```

### `submit_leaderboard`: Submit a Score

```json
{ "type": "submit_leaderboard", "mode": "weekly", "score": { "formula": "entities.total_power" } }
```

### `trigger_recipe`: Execute Another Recipe

```json
{ "type": "trigger_recipe", "selector": "energy_regeneration" }
```

### `trigger_recipes_parallel`: Execute Multiple Recipes

```json
{
  "type": "trigger_recipes_parallel",
  "continueOnError": true,
  "selectors": [
    "energy_regeneration",
    "brick_generation",
    "coin_generation"
  ]
}
```

### `disable_recipe`: Prevent Future Execution

```json
{ "type": "disable_recipe", "selector": "initial_player_state" }
```

### `assign_to_slot`: Assign Item to Slot Container

```json
{
  "type": "assign_to_slot",
  "container": "deck_1",
  "slot": "unit_1",
  "item": "unit_human_swordman"
}
```

### `send_inbox_message`: Send a Message (Preferred)

Sends a message to a player through the inbox broker. Requires an `inbox.templates` entry in your game `config.json` that defines the named template.

```json
{
  "type": "send_inbox_message",
  "recipientProfileId": "{{inputs.targetId}}",
  "template": "battle_challenge",
  "params": { "challengerName": "{{metadata.displayName}}" }
}
```

### `send_notification`: Push Notification (Deprecated)

{% hint style="warning" %}
`send_notification` is deprecated. Use the `send_inbox_message` effect (with an `inbox.templates` entry) instead.
{% endhint %}

See [Server-Authoritative API: Push Notifications](/rundot-docs/v5.21.0/readme/server_authoritative.md#push-notifications) for full details.

```json
{
  "type": "send_notification",
  "recipientProfileId": "{{inputs.targetId}}",
  "title": "Battle Challenge!",
  "body": "A player has challenged you!"
}
```

***

## Concurrency

Controls how many instances of a recipe can run simultaneously. `concurrency` accepts a constraint mode (`"single"`, `"unlimited"`, or a number) or a named group string.

| Value         | Behavior                                                                                                                                                                         |
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `"single"`    | Only one instance at a time. New executions conflict if one is active.                                                                                                           |
| `"unlimited"` | No concurrency cap; runs never conflict.                                                                                                                                         |
| `number`      | Up to N concurrent runs of this recipe; the (N+1)th conflicts.                                                                                                                   |
| `"<group>"`   | Any other string is a concurrency group identifier (e.g., `"global:building_upgrade"`). The string is stored on the run as its group; `reduce_run_expiry` targets runs by group. |

When concurrency is violated, `onConflict` decides what happens:

| `onConflict`       | Behavior                                                                                                                      |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------- |
| `"fail"` (default) | Reject the new execution.                                                                                                     |
| `"queue"`          | Enqueue the new run behind the active one (status `queued`, with a `queuePosition`); it starts when the active run completes. |
| `"replace"`        | Cancel the active run and supersede it with the new one.                                                                      |

Pair `"single"` with `"onConflict": "fail"` when you want a clear rejection on the client.

```json
{
  "upgrade_building": {
    "concurrency": "single",
    "onConflict": "fail",
    "duration": 300000
  }
}
```

With this config, starting a second upgrade while one is active fails.

***

## Multiplayer Recipes

Recipes with `scope: "room"` or `scope: "actor"` can carry a `multiplayer` block for competitive (PVP) and cooperative (guild-style) play.

```json
{
  "guild_build_wall": {
    "scope": "room",
    "duration": 3600000,
    "multiplayer": {
      "minPlayers": 2,
      "maxPlayers": 10,
      "cooperative": {
        "sharedRequirements": { "currency_bricks": 1000 },
        "sharedOutputs": { "building_wall": 1 },
        "personalOutputs": { "currency_coins": 50 },
        "rewardDistribution": { "method": "proportional", "minShare": 0.1 }
      }
    }
  }
}
```

| Field                       | Description                                                                                                                                                                                                             |
| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `minPlayers` / `maxPlayers` | Participant bounds for the recipe.                                                                                                                                                                                      |
| `commitmentWindow`          | Fraction (0 to 1) of the duration during which players can still join/contribute. Default `1.0` (entire duration).                                                                                                      |
| `competitive`               | PVP config: `battleType: "powerBattle"`, a `powerBattle` formula (per-unit `power` plus `vsBonus`), optional `returnRates`, and `winnerRewards` / `participantRewards`.                                                 |
| `cooperative`               | Guild config: `sharedRequirements` (pooled inputs), `sharedOutputs` (granted to the room), `personalOutputs` (granted per participant), and `rewardDistribution` (`equal` or `proportional`, with optional `minShare`). |

***

## Auto-Restart & Offline Catch-Up

For recipes that repeat on a timer (energy regen, resource production):

```json
{
  "energy_regeneration": {
    "duration": 600000,
    "autoRestart": true,
    "concurrency": "single",
    "onConflict": "fail",
    "maxOfflineExecutionPeriod": 604800,
    "inputs": {},
    "outputs": { "energy_current": 1 },
    "maxRestartCondition": {
      "entity": "energy_current",
      "maxValue": 20
    }
  }
}
```

| Field                          | Description                                                                                                                                        |
| ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `autoRestart`                  | Re-execute immediately after each completion.                                                                                                      |
| `autoRestartAtUtcMidnight`     | When `true`, re-run aligned to the next UTC midnight instead of immediately. Use for daily resets.                                                 |
| `maxRestartCondition.entity`   | Entity to check before restarting.                                                                                                                 |
| `maxRestartCondition.maxValue` | Numeric threshold. Auto-restart stops once the entity reaches this value. Both `entity` and `maxValue` must be present for the condition to apply. |
| `maxOfflineExecutionPeriod`    | Maximum **seconds** of offline time to catch up. `604800` = 7 days (`86400` = 24 hours).                                                           |

When a player returns after being offline, the server calculates how many cycles would have completed and grants the accumulated outputs (capped by `maxRestartCondition`).

***

## Triggers

Triggers automatically execute a recipe when watched entities change:

```json
{
  "upgrade_brick_storage_capacity": {
    "duration": 0,
    "trigger": {
      "formula": "true",
      "watchEntities": ["building_brick_storage", "building_town_hall"]
    },
    "inputs": {},
    "outputs": {
      "brick_storage_max": {
        "formula": "(500 + (entities.building_brick_storage * 500) + (entities.building_town_hall * 500)) - entities.brick_storage_max"
      }
    },
    "clientViewable": false,
    "scope": "player"
  }
}
```

The recipe auto-fires whenever `building_brick_storage` or `building_town_hall` changes. Use this for derived stats that should recalculate when their dependencies change.

***

## Formulas

Many recipe fields accept formula expressions instead of fixed values.

### Syntax

Formulas are JavaScript-like expressions evaluated server-side:

```
"formula": "floor(baseCost * costMultiplier ^ level)"
```

### Available Functions

`floor`, `ceil`, `round`, `min`, `max`, `abs`

### Variables

Formulas can reference variables defined in a `variables` block:

```json
{
  "formula": "baseCost * costMultiplier ^ level",
  "variables": {
    "baseCost": { "entity": "{{entity}}", "field": "metadata.baseCost" },
    "costMultiplier": { "entity": "{{entity}}", "field": "metadata.costMultiplier" },
    "level": { "inventory": "{{entity}}" }
  }
}
```

| Variable Source       | Syntax                                                            | Resolves To                                         |
| --------------------- | ----------------------------------------------------------------- | --------------------------------------------------- |
| Entity metadata field | `{ "entity": "building_barracks", "field": "metadata.baseCost" }` | Value from entity's metadata                        |
| Current inventory     | `{ "inventory": "energy_current" }`                               | Player's current quantity                           |
| Inline entities       | `entities.energy_current`                                         | Same as inventory, used directly in formula strings |

### Template Variables

Available in entity-scoped recipes:

| Variable                           | Description                            |
| ---------------------------------- | -------------------------------------- |
| `{{entity}}`                       | The entity ID passed at execution time |
| `{{profileId}}` / `{{executorId}}` | Player who triggered the recipe        |
| `{{roomId}}`                       | Current room ID (room-scoped recipes)  |
| `{{inputs.<key>}}`                 | Recipe input values                    |
| `{{results.<key>}}`                | Results from earlier effects           |
| `{{entities.<id>}}`                | Current inventory quantity             |
| `{{now}}`                          | Current timestamp (ms)                 |
| `{{metadata.<field>}}`             | Entity metadata (of the scoped entity) |

***

## Loot Tables

Loot tables define randomized reward pools.

### Weighted Table

Each entry has an explicit weight. Higher weight = more likely.

```json
{
  "faction_orcs_pack": {
    "type": "weighted",
    "entries": [
      { "weight": 25, "outcomes": [{ "entityId": "unit_orc_goblin", "quantity": 1 }] },
      { "weight": 20, "outcomes": [{ "entityId": "unit_orc_warrior", "quantity": 1 }] },
      { "weight": 10, "outcomes": [{ "entityId": "unit_orc_berserker", "quantity": 1 }] }
    ],
    "newItemBias": {
      "enabled": true,
      "biasMultiplier": 3.0
    }
  }
}
```

### Uniform (Tag-Filtered) Table

Dynamically selects from all entities matching tag filters. No explicit entries needed.

```json
{
  "common_units": {
    "type": "uniform",
    "filter": {
      "includeTags": ["unit", "collectible", "rarity:common"],
      "entityGuards": [
        { "entity": "{{metadata.unlockRequirement}}", "min": 1 }
      ]
    },
    "newItemBias": {
      "enabled": true,
      "biasMultiplier": 2.0
    }
  }
}
```

### Guaranteed Table

Fixed drops: always grants the specified items.

```json
{
  "common_pack_coins": {
    "type": "guaranteed",
    "entries": [
      { "entityId": "currency_coins", "quantity": { "min": 100, "max": 300 } }
    ]
  }
}
```

### Table Chaining

Weighted entries can reference other tables via `tableRef` instead of defining outcomes inline:

```json
{
  "common_pack_cards": {
    "type": "weighted",
    "entries": [
      { "tableRef": "common_units", "weight": 75 },
      { "tableRef": "rare_units", "weight": 20 },
      { "tableRef": "epic_units", "weight": 4.5 },
      { "tableRef": "legendary_units", "weight": 0.5 }
    ]
  }
}
```

### Loot Table Options

| Field                        | Type                                                      | Description                                                                                             |
| ---------------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| `type`                       | `"weighted" \| "uniform" \| "guaranteed" \| "sequential"` | How entries are selected. `sequential` drops entries in order (for milestone/sequential rewards).       |
| `entries`                    | `array`                                                   | Items, weights, and outcomes (weighted/guaranteed/sequential) or omitted (uniform).                     |
| `filter`                     | `object`                                                  | Tag-based entity selection (uniform tables).                                                            |
| `filter.includeTags`         | `string[]`                                                | Entity must have ALL of these tags.                                                                     |
| `filter.excludeTags`         | `string[]`                                                | Entity must have NONE of these tags.                                                                    |
| `filter.requireUnlocked`     | `boolean`                                                 | Only include entities the player has unlocked (paired with `unlockPrefix`).                             |
| `filter.unlockPrefix`        | `string`                                                  | Entity-ID prefix that marks unlock status (e.g., `"unlocked_"`).                                        |
| `filter.rarityWeights`       | `object`                                                  | Per-tag weight overrides for tag-filtered tables (e.g., `{ "rarity:common": 100, "rarity:rare": 20 }`). |
| `filter.entityGuards`        | `array`                                                   | Additional per-entity checks. Each is `{ entity, min?, max? }` with template substitution.              |
| `newItemBias`                | `object`                                                  | Increase probability of items the player hasn't collected yet.                                          |
| `newItemBias.biasMultiplier` | `number`                                                  | Multiplier applied to unowned items (e.g., `2.0` = 2x more likely).                                     |
| `wildcardConversion`         | `object`                                                  | Chance to convert drops into wildcard entities by rarity tag.                                           |

### Wildcard Conversion

A percentage chance to replace a rolled item with a wildcard entity:

```json
"wildcardConversion": {
  "enabled": true,
  "rates": {
    "rarity:common": 0.02,
    "rarity:rare": 0.03,
    "rarity:epic": 0.04,
    "rarity:legendary": 0.05
  },
  "wildcardEntities": {
    "rarity:common": "wildcard_common",
    "rarity:rare": "wildcard_rare",
    "rarity:epic": "wildcard_epic",
    "rarity:legendary": "wildcard_legendary"
  }
}
```

***

## Lifecycle

The `lifecycle` block defines hooks that run at specific points in a player's lifecycle.

### `onStart`

Runs once when a new player first opens the game. Use it to set up initial state.

```json
{
  "simulation": {
    "lifecycle": {
      "onStart": "initial_player_state"
    },
    "recipes": {
      "initial_player_state": {
        "duration": 0,
        "inputs": {},
        "outputs": {},
        "beginEffects": [
          { "type": "disable_recipe", "selector": "initial_player_state" },
          { "type": "set", "entity": "energy_current", "value": 20 },
          { "type": "set", "entity": "currency_coins", "value": 500 },
          {
            "type": "assign_to_slot",
            "container": "deck_1",
            "slot": "unit_1",
            "item": "unit_human_swordman"
          },
          { "type": "trigger_recipe", "selector": "start_recurring_timers" }
        ],
        "scope": "player"
      }
    }
  }
}
```

Key pattern: the `onStart` recipe disables itself (`disable_recipe`) to prevent re-execution, sets initial entity values, assigns starting items to slots, and triggers recurring timers.

{% hint style="info" %}
Use `startsDisabled: true` on the init recipe as a cleaner alternative to the self-disabling `disable_recipe` pattern. The recipe stays inert until an `enable_recipe` effect turns it on.
{% endhint %}

### `onReset`

Runs after a player reset (the `resetStateAsync` SDK method). If `onReset` is not specified, the `onStart` recipe runs instead.

```json
{
  "simulation": {
    "lifecycle": {
      "onStart": "initial_player_state",
      "onReset": "reset_player_state"
    }
  }
}
```

***

## Best Practices

* Split config across multiple files by domain in `.rundot/simulation/` (e.g., `energy-system.json`, `boosters.json`). The platform merges them at deploy time.
* Use `tags` liberally; they power subscriptions, loot table filters, and client-side discovery.
* Set `clientViewable: true` only on entities and recipes the client needs. Keep internal state hidden.
* Use `metadata` for display data and formula variables. It's free-form and available everywhere.
* Use `formula` expressions for scaling costs and dynamic outputs rather than creating separate recipes per level.
* Use entity-scoped recipes (`{{entity}}`) to avoid duplicating recipes for every item/building/card.
* Always `disable_recipe` on initialization recipes to prevent accidental re-execution.
* Set `maxOfflineExecutionPeriod` (in seconds) on auto-restart recipes to prevent unbounded offline accumulation.
* Use `maxRestartCondition` to cap regeneration (e.g., stop energy regen at max energy).


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://series-1.gitbook.io/rundot-docs/v5.21.0/readme/simulation_config.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
