162 lines
5.1 KiB
Markdown
162 lines
5.1 KiB
Markdown
|
|
# Scenarios + effects — `client.effects`
|
||
|
|
|
||
|
|
Scenarios are server-authored node graphs (configured in the dashboard) that
|
||
|
|
run per player. The SDK's scenario runtime is deliberately **not** part of the
|
||
|
|
public client surface: scenario nodes surface as typed effects through
|
||
|
|
`client.effects` (source: `src/effects/EffectsCenter.ts`), and the engine
|
||
|
|
itself loads lazily on first login.
|
||
|
|
|
||
|
|
## Lifecycle
|
||
|
|
|
||
|
|
- The runtime starts after login: scenario state is restored from persistence,
|
||
|
|
then the login event (`'player_login'` by default) is fired to trigger
|
||
|
|
event-driven scenarios.
|
||
|
|
- Runs persist to IndexedDB by default, so waits and pending nodes survive
|
||
|
|
page reloads. On restore, each run is reconciled with the server
|
||
|
|
(`unknown_run` / `expired` runs are dropped).
|
||
|
|
- `logout()` / `dispose()` clear all runs and persisted state.
|
||
|
|
|
||
|
|
### Runtime options (`RudderClientOptions.runtime`)
|
||
|
|
|
||
|
|
```ts
|
||
|
|
runtime?: {
|
||
|
|
// Scenario plan persistence. undefined = default IndexedDB store (created
|
||
|
|
// lazily); null = disable persistence entirely; or pass a PlanStateStore.
|
||
|
|
planStateStore?: PlanStateStore | null;
|
||
|
|
|
||
|
|
// 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<void>` 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;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### `NotificationEffect` — a notification node became active
|
||
|
|
|
||
|
|
```ts
|
||
|
|
{ readonly title: string; readonly message: string; done(): Promise<void> }
|
||
|
|
```
|
||
|
|
|
||
|
|
### `StoreOfferEffect` — a store node became active
|
||
|
|
|
||
|
|
```ts
|
||
|
|
{
|
||
|
|
readonly store: ShopHandle;
|
||
|
|
readonly offers: readonly OfferHandle[];
|
||
|
|
readonly message?: string;
|
||
|
|
buy(offer: OfferHandle, options?: BuyOptions): Promise<PurchaseOfferResponse>;
|
||
|
|
dismiss(): Promise<void>; // declines the offer and advances the run
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### `LeaderboardEffect` — a leaderboard node became active
|
||
|
|
|
||
|
|
```ts
|
||
|
|
{ end(): Promise<void>; rewardClaimed(): Promise<void> }
|
||
|
|
```
|
||
|
|
|
||
|
|
### `ConfigChangedEffect` — a `remote_config_override` node patched config
|
||
|
|
|
||
|
|
```ts
|
||
|
|
{ readonly key: string } // client.remoteConfig.get(key) already returns the override
|
||
|
|
```
|
||
|
|
|
||
|
|
### `WaitEffect` — a wait node became active
|
||
|
|
|
||
|
|
```ts
|
||
|
|
{ readonly deadlineUtc: Date } // run resumes automatically at the deadline
|
||
|
|
```
|
||
|
|
|
||
|
|
### `QuestEffect` — a scenario quest node became active
|
||
|
|
|
||
|
|
```ts
|
||
|
|
{
|
||
|
|
readonly name: string;
|
||
|
|
readonly objectives: ReadonlyArray<Record<string, unknown>>;
|
||
|
|
reportProgress(objectiveId: string, amount?: number): Promise<void>;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
The node auto-completes server-side once every objective is satisfied; the SDK
|
||
|
|
crosses the `onComplete` boundary itself when the server reports completion.
|
||
|
|
|
||
|
|
### `BattlePassEffect` — a battle pass node became active
|
||
|
|
|
||
|
|
```ts
|
||
|
|
{
|
||
|
|
getProgress(): Promise<GetBattlePassProgressResponse>;
|
||
|
|
addXp(source: string, amount: number): Promise<AddBattlePassXpResponse>;
|
||
|
|
claimReward(level: number, track?: 'free' | 'premium'): Promise<ClaimBattlePassRewardResponse>;
|
||
|
|
purchasePremium(): Promise<PurchaseBattlePassPremiumResponse>;
|
||
|
|
levelUp(): Promise<void>;
|
||
|
|
end(): Promise<void>;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
These wrap `client.battlePass` with the run's `scenarioId`/`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<void> }
|
||
|
|
```
|
||
|
|
|
||
|
|
### Run lifecycle effects
|
||
|
|
|
||
|
|
```ts
|
||
|
|
interface ScenarioCompletedEffect { readonly runId: string; readonly scenarioId: string }
|
||
|
|
interface ScenarioFailedEffect {
|
||
|
|
readonly runId: string; readonly scenarioId: string; readonly nodeId: string;
|
||
|
|
readonly error: Error; // transport gave up, or unsupported node type
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
Always subscribe to `onScenarioFailed` — otherwise run failures surface only
|
||
|
|
as console warnings.
|
||
|
|
|
||
|
|
## Supported node types
|
||
|
|
|
||
|
|
`wait`, `remote_config_override`, `notification`, `store`, `leaderboard`,
|
||
|
|
`quest`, `battlepass`, `battlepass_level`. Any other node type fails the run
|
||
|
|
(surfaced via `onScenarioFailed`).
|
||
|
|
|
||
|
|
## Reliability notes
|
||
|
|
|
||
|
|
- Node completion is idempotent client-side (completed handles are tracked).
|
||
|
|
- Transient boundary failures (network error, 5xx) leave the node active for
|
||
|
|
retry on reconnect; terminal server errors fail the run.
|
||
|
|
- 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`).
|