From 5517834dd48ac0aaa61c8a8749a2589ef54f9871 Mon Sep 17 00:00:00 2001 From: Le Serjant Date: Thu, 19 Mar 2026 13:24:21 +0000 Subject: [PATCH] Fix #28: stash stats periodically so beforeunload sends real scores Module._analyticsLastStats was only set when the C shutdown path ran (analytics_session_end). When a player closes the browser tab mid-game, the beforeunload fallback found _analyticsLastStats null and sent score 0 to the backend. Add analytics_stash_stats() which updates _analyticsLastStats every ~1 second during gameplay. The beforeunload handler now always has current stats to send. --- src/game/analytics.c | 28 ++++++++++++++++++++++++++++ src/game/analytics.h | 5 +++++ src/main.c | 9 +++++++++ 3 files changed, 42 insertions(+) diff --git a/src/game/analytics.c b/src/game/analytics.c index 7b40b33..30c1ebe 100644 --- a/src/game/analytics.c +++ b/src/game/analytics.c @@ -167,6 +167,20 @@ EM_JS(void, js_analytics_send_end, (int score, int level_reached, doEnd(sid, endReason, score, level_reached, lives_used, duration_secs); }); +/* Stash current stats into Module._analyticsLastStats so the + * beforeunload fallback sends real data if the tab is closed. */ +EM_JS(void, js_analytics_stash_stats, (int score, int level_reached, + int lives_used, int duration_secs), { + if (!Module._analyticsUrl || !Module._analyticsSessionId) return; + Module._analyticsLastStats = JSON.stringify({ + score: score, + level_reached: level_reached > 0 ? level_reached : 1, + lives_used: lives_used, + duration_seconds: duration_secs, + end_reason: 'quit' + }); +}); + /* ── C wrappers ─────────────────────────────────────────────────── */ void analytics_init(void) { @@ -188,6 +202,16 @@ void analytics_session_end(GameStats *stats, const char *end_reason) { ); } +void analytics_stash_stats(GameStats *stats) { + stats_update_score(stats); + js_analytics_stash_stats( + stats->score, + stats->levels_completed > 0 ? stats->levels_completed : 1, + stats->deaths, + (int)stats->time_elapsed + ); +} + #else /* ── Non-WASM stubs ─────────────────────────────────────────────── */ @@ -202,4 +226,8 @@ void analytics_session_end(GameStats *stats, const char *end_reason) { (void)end_reason; } +void analytics_stash_stats(GameStats *stats) { + (void)stats; +} + #endif /* __EMSCRIPTEN__ */ diff --git a/src/game/analytics.h b/src/game/analytics.h index cc55822..8bf99b8 100644 --- a/src/game/analytics.h +++ b/src/game/analytics.h @@ -15,4 +15,9 @@ void analytics_session_start(void); * end_reason: "death", "quit", "timeout", or "completed". */ void analytics_session_end(GameStats *stats, const char *end_reason); +/* Stash current stats for the beforeunload fallback so that + * closing the browser tab sends real data instead of zeros. + * Call periodically from the game loop. No-op on non-WASM. */ +void analytics_stash_stats(GameStats *stats); + #endif /* JNR_ANALYTICS_H */ diff --git a/src/main.c b/src/main.c index a718758..e686464 100644 --- a/src/main.c +++ b/src/main.c @@ -55,6 +55,8 @@ static int s_mars_depth = 0; /* ── Analytics / stats tracking ── */ static GameStats s_stats; static bool s_session_active = false; +static int s_stash_tick = 0; /* frames since last analytics stash */ +#define ANALYTICS_STASH_INTERVAL 60 /* stash stats every ~1 second */ /* ── Pause menu state ── */ #define PAUSE_ITEM_COUNT 3 @@ -262,6 +264,7 @@ static void begin_session(void) { stats_set_active(&s_stats); analytics_session_start(); s_session_active = true; + s_stash_tick = 0; } static void end_session(const char *reason) { @@ -581,6 +584,12 @@ static void game_update(float dt) { /* Accumulate play time */ if (s_session_active) { s_stats.time_elapsed += dt; + + /* Periodically stash stats for the beforeunload fallback. */ + if (++s_stash_tick >= ANALYTICS_STASH_INTERVAL) { + s_stash_tick = 0; + analytics_stash_stats(&s_stats); + } } /* Check for level exit transition */