Add analytics integration with Horchposten backend
Implements session-based analytics tracking that sends gameplay
stats to the Horchposten API. Adds stats.{c,h} for accumulating
per-session metrics (kills, deaths, shots, dashes, jumps, pickups,
damage, time) and analytics.{c,h} with EM_JS bridge for fetch()
calls to the backend. Client ID is persisted in localStorage.
Session start/end hooks are wired into all game lifecycle events
(level transitions, restart, quit, tab close via sendBeacon).
Analytics URL/key are configured via data attributes on the canvas
container. Non-WASM builds compile with no-op stubs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
46
src/main.c
46
src/main.c
@@ -4,6 +4,8 @@
|
||||
#include "game/level.h"
|
||||
#include "game/levelgen.h"
|
||||
#include "game/editor.h"
|
||||
#include "game/stats.h"
|
||||
#include "game/analytics.h"
|
||||
#include "config.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
@@ -46,6 +48,10 @@ static int s_station_depth = 0;
|
||||
static int s_mars_depth = 0;
|
||||
#define MARS_BASE_GEN_COUNT 2
|
||||
|
||||
/* ── Analytics / stats tracking ── */
|
||||
static GameStats s_stats;
|
||||
static bool s_session_active = false;
|
||||
|
||||
/* ── Pause menu state ── */
|
||||
#define PAUSE_ITEM_COUNT 3
|
||||
static int s_pause_selection = 0; /* 0=Resume, 1=Restart, 2=Quit */
|
||||
@@ -238,6 +244,21 @@ static void load_mars_base_level(void) {
|
||||
s_level_path[0] = '\0';
|
||||
}
|
||||
|
||||
/* ── Analytics session helpers ── */
|
||||
static void begin_session(void) {
|
||||
stats_reset(&s_stats);
|
||||
stats_set_active(&s_stats);
|
||||
analytics_session_start();
|
||||
s_session_active = true;
|
||||
}
|
||||
|
||||
static void end_session(const char *reason) {
|
||||
if (!s_session_active) return;
|
||||
s_session_active = false;
|
||||
stats_set_active(NULL);
|
||||
analytics_session_end(&s_stats, reason);
|
||||
}
|
||||
|
||||
/* ── Switch to editor mode ── */
|
||||
static void enter_editor(void) {
|
||||
if (s_mode == MODE_PLAY) {
|
||||
@@ -305,15 +326,19 @@ static void restart_level(void) {
|
||||
* ═══════════════════════════════════════════════════ */
|
||||
|
||||
static void game_init(void) {
|
||||
analytics_init();
|
||||
|
||||
if (s_use_editor) {
|
||||
enter_editor();
|
||||
} else if (s_use_procgen) {
|
||||
load_generated_level();
|
||||
begin_session();
|
||||
} else {
|
||||
if (!load_level_file("assets/levels/moon01.lvl")) {
|
||||
fprintf(stderr, "Failed to load level!\n");
|
||||
g_engine.running = false;
|
||||
}
|
||||
begin_session();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -347,12 +372,15 @@ static void pause_update(void) {
|
||||
break;
|
||||
case 1: /* Restart */
|
||||
s_mode = MODE_PLAY;
|
||||
end_session("quit");
|
||||
restart_level();
|
||||
begin_session();
|
||||
break;
|
||||
case 2: /* Quit */
|
||||
if (s_testing_from_editor) {
|
||||
return_to_editor();
|
||||
} else {
|
||||
end_session("quit");
|
||||
g_engine.running = false;
|
||||
}
|
||||
break;
|
||||
@@ -364,6 +392,7 @@ static void game_update(float dt) {
|
||||
/* Handle deferred level load from JS shell dropdown. */
|
||||
if (s_js_load_request && s_js_load_path[0]) {
|
||||
s_js_load_request = 0;
|
||||
end_session("quit");
|
||||
|
||||
/* Tear down whatever mode we are in. */
|
||||
if (s_mode == MODE_PLAY || s_mode == MODE_PAUSED) {
|
||||
@@ -389,6 +418,7 @@ static void game_update(float dt) {
|
||||
s_js_load_path[0] = '\0';
|
||||
|
||||
SDL_SetWindowTitle(g_engine.window, "Jump 'n Run");
|
||||
begin_session();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
@@ -437,22 +467,35 @@ static void game_update(float dt) {
|
||||
bool r_pressed = input_key_held(SDL_SCANCODE_R);
|
||||
if (r_pressed && !r_was_pressed) {
|
||||
printf("\n=== Regenerating level ===\n");
|
||||
end_session("quit");
|
||||
level_free(&s_level);
|
||||
s_gen_seed = (uint32_t)time(NULL);
|
||||
s_use_procgen = true;
|
||||
load_generated_level();
|
||||
begin_session();
|
||||
}
|
||||
r_was_pressed = r_pressed;
|
||||
|
||||
level_update(&s_level, dt);
|
||||
|
||||
/* Accumulate play time */
|
||||
if (s_session_active) {
|
||||
s_stats.time_elapsed += dt;
|
||||
}
|
||||
|
||||
/* Check for level exit transition */
|
||||
if (level_exit_triggered(&s_level)) {
|
||||
const char *target = s_level.exit_target;
|
||||
|
||||
/* Record the level completion in stats */
|
||||
if (s_session_active) {
|
||||
s_stats.levels_completed++;
|
||||
}
|
||||
|
||||
if (target[0] == '\0') {
|
||||
/* Empty target = victory / end of game */
|
||||
printf("Level complete! (no next level)\n");
|
||||
end_session("completed");
|
||||
/* Loop back to the beginning, reset progression state */
|
||||
level_free(&s_level);
|
||||
s_station_depth = 0;
|
||||
@@ -460,6 +503,7 @@ static void game_update(float dt) {
|
||||
if (!load_level_file("assets/levels/moon01.lvl")) {
|
||||
g_engine.running = false;
|
||||
}
|
||||
begin_session();
|
||||
} else if (strcmp(target, "generate") == 0) {
|
||||
/* Procedurally generated next level */
|
||||
printf("Transitioning to generated level\n");
|
||||
@@ -549,6 +593,8 @@ static void game_render(float interpolation) {
|
||||
}
|
||||
|
||||
static void game_shutdown(void) {
|
||||
end_session("quit");
|
||||
|
||||
/* Always free both — editor may have been initialized even if we're
|
||||
* currently in play mode (e.g. shutdown during test play). editor_free
|
||||
* and level_free are safe to call on zeroed/already-freed structs. */
|
||||
|
||||
Reference in New Issue
Block a user