Daily Missions System for s&box Games
Build daily missions with a server-selected mission, secure progress increments, and one-time reward claiming. This pattern fits extraction shooters, racers, fi...
# Daily Missions System for s&box Games
Build daily missions with a server-selected mission, secure progress increments, and one-time reward claiming. This pattern fits extraction shooters, racers, fishing games, PvE games, and retention-focused sandbox games.
The client never chooses the reward or goal. Network Storage picks the mission from Game Values, tracks progress on the player row, and rejects duplicate claims.
## Collection: `player_missions`
Create `collections/player_missions.collection.yml`:
```yml
sourceVersion: "1"
kind: collection
name: player_missions
description: Current daily mission state and mission currency.
collectionType: per-steamid
accessMode: endpoint
maxRecords: 1
allowRecordDelete: false
requireSaveVersion: true
rateLimits:
mode: none
rateLimitAction: reject
schema:
type: object
properties:
missionCredits:
type: number
min: 0
_ledger: true
activeDailyId:
type: string
dailyProgress:
type: number
min: 0
lastDailyDate:
type: string
claimedDailyDate:
type: string
completedDailyCount:
type: number
min: 0
```
## Game Values: mission pool
Create `collections/game_values.collection.yml`:
```yml
sourceVersion: "1"
kind: collection
name: game_values
description: Daily mission definitions.
collectionType: game_values
accessMode: endpoint
schema: {}
tables:
- id: daily_missions
name: Daily Missions
columns:
- key: id
type: string
- key: eventType
type: string
- key: displayName
type: string
- key: goal
type: number
- key: rewardCredits
type: number
- key: weight
type: number
rows:
- id: catch_25_fish
eventType: fish_caught
displayName: Catch 25 Fish
goal: 25
rewardCredits: 100
weight: 10
- id: sell_10_items
eventType: item_sold
displayName: Sell 10 Items
goal: 10
rewardCredits: 125
weight: 8
- id: win_3_matches
eventType: match_won
displayName: Win 3 Matches
goal: 3
rewardCredits: 250
weight: 4
```
## Workflow: validate active daily mission
Create `workflows/daily_active_mission.workflow.yml`:
```yml
sourceVersion: "1"
kind: workflow
id: daily_active_mission
name: Daily Active Mission
description: Ensures the player has a daily mission for the current UTC day.
params:
player:
type: object
steps:
- id: has_today
type: condition
check:
all:
- field: "{{player.activeDailyId}}"
op: exists
- field: "{{player.lastDailyDate}}"
op: "=="
value: "{{_dateUTC}}"
onFail:
status: 409
errorCode: DAILY_MISSION_NOT_ROLLED
message: Roll today's daily mission first.
returns:
missionId: "{{player.activeDailyId}}"
progress: "{{num(player.dailyProgress, 0)}}"
```
## Endpoint: `roll-daily-mission`
Create `endpoints/roll-daily-mission.endpoint.yml`:
```yml
sourceVersion: "1"
kind: endpoint
name: Roll Daily Mission
slug: roll-daily-mission
method: POST
enabled: true
input:
type: object
properties: {}
steps:
- id: player
type: read
collection: player_missions
key: "{{playerKey}}"
- id: not_rolled_today
type: condition
check:
field: "{{player.lastDailyDate}}"
op: "!="
value: "{{_dateUTC}}"
onFail:
status: 409
errorCode: DAILY_ALREADY_ROLLED
message: You already have today's daily mission.
- id: mission
type: random_select
source: values
table: daily_missions
where:
field: weight
op: ">"
value: 0
weightField: weight
- id: apply
type: write
collection: player_missions
key: "{{playerKey}}"
ops:
- op: set
path: activeDailyId
value: "{{mission.id}}"
- op: set
path: dailyProgress
value: 0
- op: set
path: lastDailyDate
value: "{{_dateUTC}}"
- op: set
path: claimedDailyDate
value: ""
response:
status: 200
body:
ok: true
missionId: "{{mission.id}}"
displayName: "{{mission.displayName}}"
goal: "{{mission.goal}}"
rewardCredits: "{{mission.rewardCredits}}"
```
## Endpoint: `add-daily-progress`
Use this from P2P/client-hosted games to increment the authenticated caller's mission. For competitive dedicated-server games, make a secret-key variant that accepts `targetSteamId`.
Create `endpoints/add-daily-progress.endpoint.yml`:
```yml
sourceVersion: "1"
kind: endpoint
name: Add Daily Progress
slug: add-daily-progress
method: POST
enabled: true
input:
type: object
properties:
eventType:
type: string
amount:
type: number
required:
- eventType
- amount
steps:
- id: player
type: read
collection: player_missions
key: "{{playerKey}}"
- id: active
type: workflow
workflow: daily_active_mission
params:
player: "{{player}}"
- id: mission
type: lookup
source: values
table: daily_missions
where:
field: id
op: "=="
value: "{{active.missionId}}"
- id: event_matches
type: condition
check:
field: "{{mission.eventType}}"
op: "=="
value: "{{input.eventType}}"
routes:
true:
action: continue
false:
action: return
status: 200
body:
ok: true
ignored: true
reason: event_type_mismatch
- id: safe_amount
type: transform
expression: "clamp(floor({{input.amount}}), 0, {{mission.goal}})"
- id: next_progress
type: transform
expression: "min({{mission.goal}}, {{active.progress}} + {{safe_amount}})"
- id: apply
type: write
collection: player_missions
key: "{{playerKey}}"
ops:
- op: set
path: dailyProgress
value: "{{next_progress}}"
response:
status: 200
body:
ok: true
missionId: "{{mission.id}}"
progress: "{{next_progress}}"
goal: "{{mission.goal}}"
```
## Endpoint: `claim-daily-mission`
Create `endpoints/claim-daily-mission.endpoint.yml`:
```yml
sourceVersion: "1"
kind: endpoint
name: Claim Daily Mission
slug: claim-daily-mission
method: POST
enabled: true
input:
type: object
properties: {}
steps:
- id: player
type: read
collection: player_missions
key: "{{playerKey}}"
- id: active
type: workflow
workflow: daily_active_mission
params:
player: "{{player}}"
- id: mission
type: lookup
source: values
table: daily_missions
where:
field: id
op: "=="
value: "{{active.missionId}}"
- id: complete
type: condition
check:
field: "{{active.progress}}"
op: ">="
value: "{{mission.goal}}"
onFail:
status: 403
errorCode: DAILY_NOT_COMPLETE
message: Mission is not complete yet.
- id: not_claimed
type: condition
check:
field: "{{player.claimedDailyDate}}"
op: "!="
value: "{{_dateUTC}}"
onFail:
status: 409
errorCode: DAILY_ALREADY_CLAIMED
message: Today's reward is already claimed.
- id: apply
type: write
collection: player_missions
key: "{{playerKey}}"
ops:
- op: inc
path: missionCredits
value: "{{mission.rewardCredits}}"
source: daily_mission
reason: "Claimed {{mission.id}}"
- op: set
path: claimedDailyDate
value: "{{_dateUTC}}"
- op: inc
path: completedDailyCount
value: 1
response:
status: 200
body:
ok: true
missionId: "{{mission.id}}"
rewardCredits: "{{mission.rewardCredits}}"
```
## C# calls from s&box
```csharp
var daily = await NetworkStorage.CallEndpoint( "roll-daily-mission" );
var claim = await NetworkStorage.CallEndpoint( "claim-daily-mission" );
```
Progress call:
```csharp
await NetworkStorage.CallEndpoint( "add-daily-progress", new
{
eventType = "fish_caught",
amount = 1
} );
```
## Recommended rate limits
| Rule | Collection | Field | Scope | Window | Limit | Action |
|------|------------|-------|-------|--------|-------|--------|
| `daily_progress_cap` | `player_missions` | `dailyProgress` | per_player | perDay | 1000 | clamp |
| `daily_credit_cap` | `player_missions` | `missionCredits` | per_player | perDay | 10000 | flag |
The date uses `{{_dateUTC}}`, so daily reset behavior is deterministic and independent of player local time.