Skip to content

Offline Earnings

Idle and incremental games compute “what happened while you were gone” at boot and reward the player. Tracking that moment cleanly tells you:

  • How often players come back, and after how long (return cadence)
  • Whether your offline-cap is set right (was_capped distribution)
  • Which resources actually accumulate offline vs. on-screen
  • Whether long-offline players churn or convert

There is no dedicated SDK module for this — it’s a custom track() event because every idle game’s economy is different. This recipe is the pattern that’s been battle-tested in the Quest Data dogfooding game.

Fire once at boot, after you’ve calculated offline duration and applied gains:

# scripts/idle_manager.gd (or wherever your offline calc lives)
func _calculate_offline_progress():
var current_time := int(Time.get_unix_time_from_system())
var offline_seconds := current_time - player_data.last_played_time
# Apply your cap (most idle games cap offline at 8h–24h)
var max_offline_seconds := BalancingManager.get_max_offline_time_seconds()
var capped_seconds := min(offline_seconds, max_offline_seconds)
# Run your offline simulation
var resource_gains := _simulate_offline_production(capped_seconds)
# Track it — even if offline_seconds is small. The notification UX
# is a separate decision; the analytics event always fires so you
# get the full distribution.
QuestData.track("offline_earnings", {
"offline_seconds": offline_seconds,
"capped_seconds": capped_seconds,
"was_capped": offline_seconds > max_offline_seconds,
"resource_gain_count": resource_gains.size(),
"expeditions_ready": expeditions_ready.size(),
})
player_data.last_played_time = current_time
save_player_data()
PropertyWhy
offline_secondsThe raw value. Build histograms: how many players return after 1h vs. 24h vs. 7d? Cohort it.
capped_secondsThe value you actually rewarded. Difference vs. offline_seconds is gameplay-visible damage.
was_cappedBoolean flag for fast filtering. WHERE was_capped = true instantly finds players hitting your cap.
resource_gain_countDiversity metric. A player whose offline earnings only touch 1 resource is mid-game; one touching 10 resources is late-game.
Game-specific extrasAny structural counters that move during offline (expeditions, recipes finished, …). Skip per-resource amounts — they explode the JSONB.
  • Per-resource amounts (gold_gained: 12345). One row per resource scales linearly with your economy and bloats event payloads. Track totals or gain_count. If you really want amounts, fire one event per resource with { resource_id, amount }.
  • true_amount vs displayed_amount. If you have a logarithmic economy with billions of gold, the dashboard will struggle to chart it. Use log10(amount) as a property if you need it.
  • The full resource_gains dict as a single property. Use resource_gain_count instead — same insight, smaller payload.

Return-time histogram (Player Explorer → Events → filter offline_earnings):

“Distribution of offline_seconds for the last 7 days, bucket by hour.”

Tells you whether your push-notification cadence matches actual return behavior.

Cap pressure (Custom event chart):

“Count of offline_earnings events grouped by was_capped per day.”

If the true slice stays above ~30% your cap is too tight — players are hitting it and feeling the missing earnings. Raise it.

Churn correlation (Funnel, advanced):

“Players who fired offline_earnings with offline_seconds > 86400 — what’s their D7 retention?”

Long-absence return events are the only signal you have for “almost-churned” players. If their retention is low, your re-engagement loop is broken.

For long-term tracking, mirror the offline state into a Player Property so segments and Remote Config can react:

QuestData.set_user_properties({
"last_offline_seconds": offline_seconds,
"lifetime_offline_count": player_data.lifetime_offline_count + 1,
})

Then create a segment “Long-absence returners” = last_offline_seconds > 86400 and feed them a Remote Config welcome bonus.

Use Player Tags for boolean state:

if offline_seconds > 7 * 86400:
QuestData.set_user_tag("returning_player")

returning_player becomes a one-click segment in the dashboard.

Nothing special — track() already does:

  • Batched HTTP delivery (10 events or 10 seconds, whichever first)
  • Offline queue persisted to user://quest_queue.save
  • Auto-retry on network failure
  • Session ID + player ID auto-attached

The “boot moment” is the trickiest timing in an idle game. Make sure your SDK is initialized before _calculate_offline_progress() runs — see the Quick Start for autoload order.