# Scenarios

Scenario graphs execute on the server. `client.Scenario` (`ScenarioService`)
only triggers an event; pending nodes surface through `client.Effects`
(`EffectsService`). Verified against the bundled `Rudder.Core.dll` and
`Samples~/Scenarios/ScenariosSample.cs`.

## Flow

Subscribe to `Effects.On*` **before** triggering, then trigger an event name
configured in the dashboard (examples: `player_login`,
`demo_round_finished`). Unsubscribe on destroy.

```csharp
client.Effects.OnNotification += effect => { /* show UI, then effect.Done() */ };
await client.Scenario.TriggerAsync("player_login");
```

API:

- `Task ScenarioService.TriggerAsync(string eventName, CancellationToken = default)` — posts the event and ingests any effects the response contains. Does not return runs.
- `void RudderClient.Update(float deltaTime)` — pumped by the `Rudder` component. Drives the 30s pending-effects heartbeat and wait-deadline checks. Do not call it from game code.

After `Auth.LoginWithDeviceAsync`, the heartbeat fetches pending effects for
the signed-in player. Effects that arrive later (or after a wait deadline)
are dispatched the same way.

## Events and effects

All effect types expose `RunId`, `ScenarioId`, `NodeId`, `Data` (`JObject`),
and `T Get<T>(string key, T defaultValue = default)` for node data.

| Event | Effect | Game must |
| --- | --- | --- |
| `OnNotification` | `NotificationEffect` | Show UI (`Title`, `Message`), then `Done()` / `DoneAsync()`. |
| `OnStoreOffer` | `StoreOfferEffect` | Show offer (`StoreSlug`, `Get("storeSlug", ...)`); on buy call `Stores.PurchaseAsync` then `Purchase()` / `PurchaseAsync()`, else `Decline()` / `DeclineAsync()`. `IsResolved` marks resolution. |
| `OnWait` | `WaitEffect` | Deadline only (`DeadlineUtc`); the server advances the run. |
| `OnQuest` | `QuestEffect` | Scenario quest node: `ReportProgress(objectiveId, amount)` / `ReportProgressAsync`. The server auto-completes the node once every objective is satisfied. Not `client.Quests`. |
| `OnLeaderboard` | `LeaderboardEffect` | `End()` / `EndAsync()`, `Claim()` / `ClaimAsync()`. `IsResolved` marks resolution. |
| `OnBattlePass` | `BattlePassEffect` | Drive the effect — see [battlepass.md](battlepass.md). |
| `OnBattlePassLevel` | `BattlePassLevelEffect` | `Claim()` / `ClaimAsync()` for the level reward; `Level` prop. |
| `OnScenarioCompleted` | `ScenarioCompletedEffect` | `RunId`, `ScenarioId`. Log / surface. |
| `OnScenarioFailed` | `ScenarioFailedEffect` | `RunId`, `ScenarioId`, `NodeId`, `Exception`. Log / surface. |

Async and sync variants are equivalent; the sync variants fire-and-forget
the same server callback. The event name must match a scenario trigger
configured in the dashboard — an unknown event simply produces no effects.
