# Scenarios + effects — `client.effects` Scenarios are server-authored node graphs (configured in the dashboard) that run per player. The server executes the graph. The SDK is a thin effects client: it sends trigger events, turns `PendingEffect` payloads into typed `client.effects` handlers, posts callbacks when the game completes an effect, and polls for pending effects. The runtime is deliberately **not** part of the public client surface (source: `src/effects/EffectsCenter.ts`) and loads lazily on first login. ## Lifecycle - After login the SDK fetches `GET /sdk/v1/scenarios/pending` (replacing any local restore), then fires the login event (`'player_login'` by default). - A heartbeat polls pending every ~30s (±20% jitter), paused while `document.hidden`. Each effect with a `waitDeadline` also schedules a local timer so waits fire on time. - An effect with an already-active `(runId, nodeId)` is not re-emitted. - `logout()` / `dispose()` stop the poll, cancel wait timers, and drop active effects. There is no local plan persistence. ### Runtime options (`RudderClientOptions.runtime`) ```ts runtime?: { // Scenario event fired automatically after login. // undefined = 'player_login'; null = fire nothing. loginEvent?: string | null; } ``` ## Subscribing Every `on*` method takes a handler `(effect) => void | Promise` and returns an unsubscribe function. Errors thrown inside handlers go to the `onEffectError` client option (default `console.error`). ```ts const off = client.effects.onNotification(async (n) => { showToast(n.title, n.message); await n.done(); // always resolve the session or the run stalls }); ``` ## Effect types ```ts interface Effects { onNotification(handler): EffectUnsubscribe; onStoreOffer(handler): EffectUnsubscribe; onLeaderboard(handler): EffectUnsubscribe; onConfigChanged(handler): EffectUnsubscribe; onWait(handler): EffectUnsubscribe; onScenarioCompleted(handler): EffectUnsubscribe; onScenarioFailed(handler): EffectUnsubscribe; onQuest(handler): EffectUnsubscribe; onBattlePass(handler): EffectUnsubscribe; onBattlePassLevel(handler): EffectUnsubscribe; } ``` `onConfigChanged` remains on the public surface but scenario `remote_config_override` nodes no longer reach the client — the server applies them. Patched values show up through `client.remoteConfig` after sync/reload. ### `NotificationEffect` — a notification node became active ```ts { readonly title: string; readonly message: string; done(): Promise } ``` ### `StoreOfferEffect` — a store node became active ```ts { readonly store: ShopHandle; readonly offers: readonly OfferHandle[]; readonly message?: string; buy(offer: OfferHandle, options?: BuyOptions): Promise; dismiss(): Promise; // declines the offer and advances the run } ``` ### `LeaderboardEffect` — a leaderboard node became active ```ts { end(): Promise; claim(): Promise; rewardClaimed(): Promise } ``` `rewardClaimed()` is a deprecated alias for `claim()`. ### `WaitEffect` — a wait node became active ```ts { readonly deadlineUtc: Date } // SDK polls pending at the deadline; the server advances the run ``` ### `QuestEffect` — a scenario quest node became active ```ts { readonly name: string; readonly objectives: ReadonlyArray>; reportProgress(objectiveId: string, amount?: number): Promise; } ``` The node auto-completes server-side once every objective is satisfied; the counter response may carry the next `PendingEffect`. ### `BattlePassEffect` — a battle pass node became active ```ts { getProgress(): Promise; addXp(source: string, amount: number): Promise; claimReward(level: number, track?: 'free' | 'premium'): Promise; purchasePremium(): Promise; levelUp(): Promise; end(): Promise; } ``` These wrap `client.battlePass` with the run's `scenarioSlug`/`nodeId`/`runId` already bound — prefer them over calling the service manually. ### `BattlePassLevelEffect` — a `battlepass_level` node (single claimable tier) ```ts { readonly level: number; claim(): Promise } ``` ### Run lifecycle effects ```ts interface ScenarioCompletedEffect { readonly runId: string; readonly scenarioSlug: string } interface ScenarioFailedEffect { readonly runId: string; readonly scenarioSlug: string; readonly nodeId: string; readonly error: Error; } ``` Always subscribe to `onScenarioFailed` — otherwise run failures surface only as console warnings. ## Supported node types `wait`, `notification`, `store`, `leaderboard`, `quest`, `battlepass`, `battlepass_level`. Any other node type fails the run (surfaced via `onScenarioFailed`). `remote_config_override` is applied server-side and is not delivered to the client. ## Reliability notes - Completion methods POST `/sdk/v1/scenarios/callback` with `{scenarioSlug, runId, nodeId, handle}` using the handles `output`, `onPurchase`, `onDecline`, `onEnd`, `onClaim`, `onComplete`, `onLevelUp`, `onPremiumPurchase`. - Transient callback failures (network error, 5xx) leave the effect active for retry; `unknown_run` / `run_expired` drop that run's effects and fire `onScenarioFailed`. - Server scenario errors use the typed codes `run_expired`, `run_not_active`, `node_not_active`, `scenario_not_active`, `unknown_run`, `early_completion`, `objectives_incomplete`, `level_not_reached`, `forbidden` (`RudderErrorCodes`).