Fix #28: Add lives system, game over screen, and fix leaderboard score 0
All checks were successful
CI / build (pull_request) Successful in 32s
All checks were successful
CI / build (pull_request) Successful in 32s
- Add 3-life system: each death costs a life, game over at 0 lives - Add game over screen with arcade-style 3-character name entry - Submit player name with score to analytics backend on game over - Fix score 0 on leaderboard by periodically stashing stats to JS so the beforeunload fallback has real data when users close the tab - Display lives counter in HUD next to health hearts - Move player respawn control from level.c to main.c via player_death_pending flag for proper lives tracking
This commit is contained in:
@@ -103,20 +103,39 @@ EM_JS(void, js_analytics_session_start, (), {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* Stash current stats into Module._analyticsLastStats so the
|
||||||
|
* beforeunload fallback has real data if the user closes the tab.
|
||||||
|
* Called periodically from C (e.g. once per second). */
|
||||||
|
EM_JS(void, js_analytics_stash_stats, (int score, int level_reached,
|
||||||
|
int lives_used, int duration_secs), {
|
||||||
|
if (!Module._analyticsUrl) 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'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
/* Internal helper: send the session-end POST (used by both the C wrapper
|
/* Internal helper: send the session-end POST (used by both the C wrapper
|
||||||
* and the beforeunload fallback). */
|
* and the beforeunload fallback). */
|
||||||
EM_JS(void, js_analytics_send_end, (int score, int level_reached,
|
EM_JS(void, js_analytics_send_end, (int score, int level_reached,
|
||||||
int lives_used, int duration_secs,
|
int lives_used, int duration_secs,
|
||||||
const char *end_reason_ptr), {
|
const char *end_reason_ptr,
|
||||||
|
const char *player_name_ptr), {
|
||||||
/* Helper that performs the actual end request given a session id. */
|
/* Helper that performs the actual end request given a session id. */
|
||||||
function doEnd(sid, endReason, score, levelReached, livesUsed, durationSecs) {
|
function doEnd(sid, endReason, playerName, score, levelReached,
|
||||||
var body = JSON.stringify({
|
livesUsed, durationSecs) {
|
||||||
|
var payload = {
|
||||||
score: score,
|
score: score,
|
||||||
level_reached: levelReached > 0 ? levelReached : 1,
|
level_reached: levelReached > 0 ? levelReached : 1,
|
||||||
lives_used: livesUsed,
|
lives_used: livesUsed,
|
||||||
duration_seconds: durationSecs,
|
duration_seconds: durationSecs,
|
||||||
end_reason: endReason
|
end_reason: endReason
|
||||||
});
|
};
|
||||||
|
if (playerName) payload.player_name = playerName;
|
||||||
|
var body = JSON.stringify(payload);
|
||||||
|
|
||||||
/* Stash stats for the beforeunload fallback */
|
/* Stash stats for the beforeunload fallback */
|
||||||
Module._analyticsLastStats = body;
|
Module._analyticsLastStats = body;
|
||||||
@@ -143,6 +162,7 @@ EM_JS(void, js_analytics_send_end, (int score, int level_reached,
|
|||||||
if (!Module._analyticsUrl) return;
|
if (!Module._analyticsUrl) return;
|
||||||
|
|
||||||
var endReason = UTF8ToString(end_reason_ptr);
|
var endReason = UTF8ToString(end_reason_ptr);
|
||||||
|
var playerName = player_name_ptr ? UTF8ToString(player_name_ptr) : '';
|
||||||
|
|
||||||
/* If session start is still in-flight, wait for it before ending. */
|
/* If session start is still in-flight, wait for it before ending. */
|
||||||
if (Module._analyticsStartPending) {
|
if (Module._analyticsStartPending) {
|
||||||
@@ -153,7 +173,8 @@ EM_JS(void, js_analytics_send_end, (int score, int level_reached,
|
|||||||
var sid = Module._analyticsSessionId;
|
var sid = Module._analyticsSessionId;
|
||||||
if (sid) {
|
if (sid) {
|
||||||
Module._analyticsSessionId = null;
|
Module._analyticsSessionId = null;
|
||||||
doEnd(sid, endReason, score, level_reached, lives_used, duration_secs);
|
doEnd(sid, endReason, playerName, score, level_reached,
|
||||||
|
lives_used, duration_secs);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -164,7 +185,8 @@ EM_JS(void, js_analytics_send_end, (int score, int level_reached,
|
|||||||
var sid = Module._analyticsSessionId;
|
var sid = Module._analyticsSessionId;
|
||||||
/* Clear synchronously before the async request to prevent races */
|
/* Clear synchronously before the async request to prevent races */
|
||||||
Module._analyticsSessionId = null;
|
Module._analyticsSessionId = null;
|
||||||
doEnd(sid, endReason, score, level_reached, lives_used, duration_secs);
|
doEnd(sid, endReason, playerName, score, level_reached,
|
||||||
|
lives_used, duration_secs);
|
||||||
});
|
});
|
||||||
|
|
||||||
/* ── C wrappers ─────────────────────────────────────────────────── */
|
/* ── C wrappers ─────────────────────────────────────────────────── */
|
||||||
@@ -177,14 +199,26 @@ void analytics_session_start(void) {
|
|||||||
js_analytics_session_start();
|
js_analytics_session_start();
|
||||||
}
|
}
|
||||||
|
|
||||||
void analytics_session_end(GameStats *stats, const char *end_reason) {
|
void analytics_session_end(GameStats *stats, const char *end_reason,
|
||||||
|
const char *player_name) {
|
||||||
stats_update_score(stats);
|
stats_update_score(stats);
|
||||||
js_analytics_send_end(
|
js_analytics_send_end(
|
||||||
stats->score,
|
stats->score,
|
||||||
stats->levels_completed > 0 ? stats->levels_completed : 1,
|
stats->levels_completed > 0 ? stats->levels_completed : 1,
|
||||||
stats->deaths,
|
stats->deaths,
|
||||||
(int)stats->time_elapsed,
|
(int)stats->time_elapsed,
|
||||||
end_reason
|
end_reason,
|
||||||
|
player_name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,9 +231,15 @@ void analytics_init(void) {
|
|||||||
|
|
||||||
void analytics_session_start(void) {}
|
void analytics_session_start(void) {}
|
||||||
|
|
||||||
void analytics_session_end(GameStats *stats, const char *end_reason) {
|
void analytics_session_end(GameStats *stats, const char *end_reason,
|
||||||
|
const char *player_name) {
|
||||||
(void)stats;
|
(void)stats;
|
||||||
(void)end_reason;
|
(void)end_reason;
|
||||||
|
(void)player_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
void analytics_stash_stats(GameStats *stats) {
|
||||||
|
(void)stats;
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif /* __EMSCRIPTEN__ */
|
#endif /* __EMSCRIPTEN__ */
|
||||||
|
|||||||
@@ -12,7 +12,14 @@ void analytics_session_start(void);
|
|||||||
|
|
||||||
/* End the current analytics session with final stats.
|
/* End the current analytics session with final stats.
|
||||||
* Computes the composite score before sending.
|
* Computes the composite score before sending.
|
||||||
* end_reason: "death", "quit", "timeout", or "completed". */
|
* end_reason: "death", "quit", "timeout", or "completed".
|
||||||
void analytics_session_end(GameStats *stats, const char *end_reason);
|
* player_name may be NULL or empty if no name was entered. */
|
||||||
|
void analytics_session_end(GameStats *stats, const char *end_reason,
|
||||||
|
const char *player_name);
|
||||||
|
|
||||||
|
/* Stash current stats into JS so the beforeunload fallback has
|
||||||
|
* real data if the user closes the tab mid-game. Call periodically
|
||||||
|
* (e.g. once per second or on level completion). No-op on native. */
|
||||||
|
void analytics_stash_stats(GameStats *stats);
|
||||||
|
|
||||||
#endif /* JNR_ANALYTICS_H */
|
#endif /* JNR_ANALYTICS_H */
|
||||||
|
|||||||
@@ -591,18 +591,14 @@ void level_update(Level *level, float dt) {
|
|||||||
/* Fallback direct exit zone check (for levels without ship exit) */
|
/* Fallback direct exit zone check (for levels without ship exit) */
|
||||||
check_exit_zones(level);
|
check_exit_zones(level);
|
||||||
|
|
||||||
/* Check for player respawn (skip if player boarded exit ship) */
|
/* Check for player death (skip if player boarded exit ship).
|
||||||
|
* Don't auto-respawn — set a flag for main.c to handle lives. */
|
||||||
if (!level->exit_ship_boarded) {
|
if (!level->exit_ship_boarded) {
|
||||||
for (int i = 0; i < level->entities.count; i++) {
|
for (int i = 0; i < level->entities.count; i++) {
|
||||||
Entity *e = &level->entities.entities[i];
|
Entity *e = &level->entities.entities[i];
|
||||||
if (e->active && e->type == ENT_PLAYER && player_wants_respawn(e)) {
|
if (e->active && e->type == ENT_PLAYER && player_wants_respawn(e)) {
|
||||||
stats_record_death();
|
stats_record_death();
|
||||||
player_respawn(e, level->map.player_spawn);
|
level->player_death_pending = true;
|
||||||
Vec2 center = vec2(
|
|
||||||
e->body.pos.x + e->body.size.x * 0.5f,
|
|
||||||
e->body.pos.y + e->body.size.y * 0.5f
|
|
||||||
);
|
|
||||||
camera_follow(&level->camera, center, vec2_zero(), dt);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -759,6 +755,18 @@ void level_render(Level *level, float interpolation) {
|
|||||||
renderer_draw_rect(pos, size, heart_color, LAYER_HUD, cam);
|
renderer_draw_rect(pos, size, heart_color, LAYER_HUD, cam);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Draw lives counter next to hearts */
|
||||||
|
{
|
||||||
|
GameStats *st = stats_get_active();
|
||||||
|
if (st && st->lives > 0) {
|
||||||
|
char lives_buf[8];
|
||||||
|
snprintf(lives_buf, sizeof(lives_buf), "x%d", st->lives);
|
||||||
|
int lx = 8 + player->max_health * 14 + 4;
|
||||||
|
font_draw_text(g_engine.renderer, lives_buf, lx, 8,
|
||||||
|
(SDL_Color){200, 200, 200, 255});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Draw jetpack charge indicators */
|
/* Draw jetpack charge indicators */
|
||||||
int charges, max_charges;
|
int charges, max_charges;
|
||||||
float recharge_pct;
|
float recharge_pct;
|
||||||
@@ -817,6 +825,17 @@ void level_render(Level *level, float interpolation) {
|
|||||||
renderer_flush(cam);
|
renderer_flush(cam);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void level_respawn_player(Level *level) {
|
||||||
|
level->player_death_pending = false;
|
||||||
|
for (int i = 0; i < level->entities.count; i++) {
|
||||||
|
Entity *e = &level->entities.entities[i];
|
||||||
|
if (e->active && e->type == ENT_PLAYER) {
|
||||||
|
player_respawn(e, level->map.player_spawn);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void level_free(Level *level) {
|
void level_free(Level *level) {
|
||||||
audio_stop_music();
|
audio_stop_music();
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ typedef struct Level {
|
|||||||
bool exit_ship_spawned; /* exit spacecraft has been spawned */
|
bool exit_ship_spawned; /* exit spacecraft has been spawned */
|
||||||
bool exit_ship_boarded; /* player has entered the ship */
|
bool exit_ship_boarded; /* player has entered the ship */
|
||||||
int exit_zone_idx; /* which exit zone the ship is for */
|
int exit_zone_idx; /* which exit zone the ship is for */
|
||||||
|
|
||||||
|
/* ── Death / game over ────────────────── */
|
||||||
|
bool player_death_pending; /* player died, awaiting main.c */
|
||||||
} Level;
|
} Level;
|
||||||
|
|
||||||
bool level_load(Level *level, const char *path);
|
bool level_load(Level *level, const char *path);
|
||||||
@@ -39,4 +42,8 @@ void level_free(Level *level);
|
|||||||
* The target path is stored in level->exit_target. */
|
* The target path is stored in level->exit_target. */
|
||||||
bool level_exit_triggered(const Level *level);
|
bool level_exit_triggered(const Level *level);
|
||||||
|
|
||||||
|
/* Respawn the player at the level's spawn point. Called by main.c
|
||||||
|
* after acknowledging a death (decrementing lives). */
|
||||||
|
void level_respawn_player(Level *level);
|
||||||
|
|
||||||
#endif /* JNR_LEVEL_H */
|
#endif /* JNR_LEVEL_H */
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ typedef struct GameStats {
|
|||||||
int damage_taken; /* total HP lost */
|
int damage_taken; /* total HP lost */
|
||||||
int damage_dealt; /* total HP dealt to enemies */
|
int damage_dealt; /* total HP dealt to enemies */
|
||||||
float time_elapsed; /* wall-clock seconds */
|
float time_elapsed; /* wall-clock seconds */
|
||||||
|
int lives; /* remaining lives (set by main) */
|
||||||
} GameStats;
|
} GameStats;
|
||||||
|
|
||||||
/* Reset all stats to zero (call at session start). */
|
/* Reset all stats to zero (call at session start). */
|
||||||
|
|||||||
199
src/main.c
199
src/main.c
@@ -27,6 +27,7 @@ typedef enum GameMode {
|
|||||||
MODE_EDITOR,
|
MODE_EDITOR,
|
||||||
MODE_PAUSED,
|
MODE_PAUSED,
|
||||||
MODE_TRANSITION,
|
MODE_TRANSITION,
|
||||||
|
MODE_GAMEOVER,
|
||||||
} GameMode;
|
} GameMode;
|
||||||
|
|
||||||
static Level s_level;
|
static Level s_level;
|
||||||
@@ -56,10 +57,22 @@ static int s_mars_depth = 0;
|
|||||||
static GameStats s_stats;
|
static GameStats s_stats;
|
||||||
static bool s_session_active = false;
|
static bool s_session_active = false;
|
||||||
|
|
||||||
|
/* ── Lives system ── */
|
||||||
|
#define STARTING_LIVES 3
|
||||||
|
static int s_lives = STARTING_LIVES;
|
||||||
|
static float s_stash_timer = 0.0f; /* timer for periodic stats stash */
|
||||||
|
#define STASH_INTERVAL 2.0f /* seconds between stats stashes */
|
||||||
|
|
||||||
/* ── Pause menu state ── */
|
/* ── Pause menu state ── */
|
||||||
#define PAUSE_ITEM_COUNT 3
|
#define PAUSE_ITEM_COUNT 3
|
||||||
static int s_pause_selection = 0; /* 0=Resume, 1=Restart, 2=Quit */
|
static int s_pause_selection = 0; /* 0=Resume, 1=Restart, 2=Quit */
|
||||||
|
|
||||||
|
/* ── Game over state ── */
|
||||||
|
#define NAME_MAX_LEN 3
|
||||||
|
static char s_go_name[NAME_MAX_LEN + 1] = "AAA"; /* player name for scoreboard */
|
||||||
|
static int s_go_cursor = 0; /* which character is selected */
|
||||||
|
static bool s_go_submitted = false; /* name has been submitted */
|
||||||
|
|
||||||
/* ── Level transition state ── */
|
/* ── Level transition state ── */
|
||||||
static TransitionState s_transition;
|
static TransitionState s_transition;
|
||||||
static char s_pending_target[ASSET_PATH_MAX] = {0}; /* exit target stashed during transition */
|
static char s_pending_target[ASSET_PATH_MAX] = {0}; /* exit target stashed during transition */
|
||||||
@@ -259,16 +272,19 @@ static void load_mars_base_level(void) {
|
|||||||
/* ── Analytics session helpers ── */
|
/* ── Analytics session helpers ── */
|
||||||
static void begin_session(void) {
|
static void begin_session(void) {
|
||||||
stats_reset(&s_stats);
|
stats_reset(&s_stats);
|
||||||
|
s_lives = STARTING_LIVES;
|
||||||
|
s_stats.lives = s_lives;
|
||||||
|
s_stash_timer = 0.0f;
|
||||||
stats_set_active(&s_stats);
|
stats_set_active(&s_stats);
|
||||||
analytics_session_start();
|
analytics_session_start();
|
||||||
s_session_active = true;
|
s_session_active = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void end_session(const char *reason) {
|
static void end_session(const char *reason, const char *player_name) {
|
||||||
if (!s_session_active) return;
|
if (!s_session_active) return;
|
||||||
s_session_active = false;
|
s_session_active = false;
|
||||||
stats_set_active(NULL);
|
stats_set_active(NULL);
|
||||||
analytics_session_end(&s_stats, reason);
|
analytics_session_end(&s_stats, reason, player_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Switch to editor mode ── */
|
/* ── Switch to editor mode ── */
|
||||||
@@ -342,7 +358,7 @@ static void dispatch_level_load(const char *target) {
|
|||||||
if (target[0] == '\0') {
|
if (target[0] == '\0') {
|
||||||
/* Empty target = victory / end of game. */
|
/* Empty target = victory / end of game. */
|
||||||
printf("Level complete! (no next level)\n");
|
printf("Level complete! (no next level)\n");
|
||||||
end_session("completed");
|
end_session("completed", NULL);
|
||||||
level_free(&s_level);
|
level_free(&s_level);
|
||||||
s_station_depth = 0;
|
s_station_depth = 0;
|
||||||
s_mars_depth = 0;
|
s_mars_depth = 0;
|
||||||
@@ -430,7 +446,7 @@ static void pause_update(void) {
|
|||||||
break;
|
break;
|
||||||
case 1: /* Restart */
|
case 1: /* Restart */
|
||||||
s_mode = MODE_PLAY;
|
s_mode = MODE_PLAY;
|
||||||
end_session("quit");
|
end_session("quit", NULL);
|
||||||
restart_level();
|
restart_level();
|
||||||
begin_session();
|
begin_session();
|
||||||
break;
|
break;
|
||||||
@@ -438,7 +454,7 @@ static void pause_update(void) {
|
|||||||
if (s_testing_from_editor) {
|
if (s_testing_from_editor) {
|
||||||
return_to_editor();
|
return_to_editor();
|
||||||
} else {
|
} else {
|
||||||
end_session("quit");
|
end_session("quit", NULL);
|
||||||
g_engine.running = false;
|
g_engine.running = false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -462,11 +478,11 @@ static void game_update(float dt) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
end_session("quit");
|
end_session("quit", NULL);
|
||||||
|
|
||||||
/* Tear down whatever mode we are in. */
|
/* Tear down whatever mode we are in. */
|
||||||
if (s_mode == MODE_PLAY || s_mode == MODE_PAUSED
|
if (s_mode == MODE_PLAY || s_mode == MODE_PAUSED
|
||||||
|| s_mode == MODE_TRANSITION) {
|
|| s_mode == MODE_TRANSITION || s_mode == MODE_GAMEOVER) {
|
||||||
debuglog_set_level(NULL, NULL);
|
debuglog_set_level(NULL, NULL);
|
||||||
transition_reset(&s_transition);
|
transition_reset(&s_transition);
|
||||||
level_free(&s_level);
|
level_free(&s_level);
|
||||||
@@ -511,6 +527,11 @@ static void game_update(float dt) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (s_mode == MODE_GAMEOVER) {
|
||||||
|
gameover_update();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (s_mode == MODE_TRANSITION) {
|
if (s_mode == MODE_TRANSITION) {
|
||||||
transition_update(&s_transition, dt, &s_level.camera);
|
transition_update(&s_transition, dt, &s_level.camera);
|
||||||
|
|
||||||
@@ -566,7 +587,7 @@ static void game_update(float dt) {
|
|||||||
bool r_pressed = input_key_held(SDL_SCANCODE_R);
|
bool r_pressed = input_key_held(SDL_SCANCODE_R);
|
||||||
if (r_pressed && !r_was_pressed) {
|
if (r_pressed && !r_was_pressed) {
|
||||||
printf("\n=== Regenerating level ===\n");
|
printf("\n=== Regenerating level ===\n");
|
||||||
end_session("quit");
|
end_session("quit", NULL);
|
||||||
debuglog_set_level(NULL, NULL);
|
debuglog_set_level(NULL, NULL);
|
||||||
level_free(&s_level);
|
level_free(&s_level);
|
||||||
s_gen_seed = (uint32_t)time(NULL);
|
s_gen_seed = (uint32_t)time(NULL);
|
||||||
@@ -583,6 +604,25 @@ static void game_update(float dt) {
|
|||||||
s_stats.time_elapsed += dt;
|
s_stats.time_elapsed += dt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Check for player death — decrement lives or trigger game over */
|
||||||
|
if (s_level.player_death_pending) {
|
||||||
|
s_level.player_death_pending = false;
|
||||||
|
s_lives--;
|
||||||
|
s_stats.lives = s_lives;
|
||||||
|
if (s_lives <= 0) {
|
||||||
|
enter_gameover();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
level_respawn_player(&s_level);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Periodically stash stats so beforeunload has real data */
|
||||||
|
s_stash_timer += dt;
|
||||||
|
if (s_stash_timer >= STASH_INTERVAL && s_session_active) {
|
||||||
|
analytics_stash_stats(&s_stats);
|
||||||
|
s_stash_timer = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
/* Check for level exit transition */
|
/* Check for level exit transition */
|
||||||
if (level_exit_triggered(&s_level)) {
|
if (level_exit_triggered(&s_level)) {
|
||||||
const char *target = s_level.exit_target;
|
const char *target = s_level.exit_target;
|
||||||
@@ -647,6 +687,140 @@ static void pause_render(void) {
|
|||||||
SDL_SetRenderDrawBlendMode(r, SDL_BLENDMODE_NONE);
|
SDL_SetRenderDrawBlendMode(r, SDL_BLENDMODE_NONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Enter game over mode ── */
|
||||||
|
static void enter_gameover(void) {
|
||||||
|
s_mode = MODE_GAMEOVER;
|
||||||
|
s_go_cursor = 0;
|
||||||
|
s_go_submitted = false;
|
||||||
|
memcpy(s_go_name, "AAA", NAME_MAX_LEN + 1);
|
||||||
|
|
||||||
|
/* Finalize score for display */
|
||||||
|
stats_update_score(&s_stats);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Game over: handle name entry input ── */
|
||||||
|
static void gameover_update(void) {
|
||||||
|
if (s_go_submitted) {
|
||||||
|
/* After submission, wait for confirm to restart */
|
||||||
|
bool confirm = input_pressed(ACTION_JUMP)
|
||||||
|
|| input_key_pressed(SDL_SCANCODE_RETURN)
|
||||||
|
|| input_key_pressed(SDL_SCANCODE_RETURN2);
|
||||||
|
if (confirm) {
|
||||||
|
end_session("death", s_go_name);
|
||||||
|
debuglog_set_level(NULL, NULL);
|
||||||
|
level_free(&s_level);
|
||||||
|
s_station_depth = 0;
|
||||||
|
s_mars_depth = 0;
|
||||||
|
s_mode = MODE_PLAY;
|
||||||
|
if (!load_level_file("assets/levels/moon01.lvl")) {
|
||||||
|
g_engine.running = false;
|
||||||
|
}
|
||||||
|
begin_session();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cycle letter up/down */
|
||||||
|
if (input_pressed(ACTION_UP)) {
|
||||||
|
s_go_name[s_go_cursor]++;
|
||||||
|
if (s_go_name[s_go_cursor] > 'Z') s_go_name[s_go_cursor] = 'A';
|
||||||
|
}
|
||||||
|
if (input_pressed(ACTION_DOWN)) {
|
||||||
|
s_go_name[s_go_cursor]--;
|
||||||
|
if (s_go_name[s_go_cursor] < 'A') s_go_name[s_go_cursor] = 'Z';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Move cursor left/right */
|
||||||
|
if (input_pressed(ACTION_LEFT)) {
|
||||||
|
if (s_go_cursor > 0) s_go_cursor--;
|
||||||
|
}
|
||||||
|
if (input_pressed(ACTION_RIGHT)) {
|
||||||
|
if (s_go_cursor < NAME_MAX_LEN - 1) s_go_cursor++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Confirm name */
|
||||||
|
bool confirm = input_pressed(ACTION_JUMP)
|
||||||
|
|| input_key_pressed(SDL_SCANCODE_RETURN)
|
||||||
|
|| input_key_pressed(SDL_SCANCODE_RETURN2);
|
||||||
|
if (confirm) {
|
||||||
|
s_go_submitted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Game over: render overlay ── */
|
||||||
|
static void gameover_render(void) {
|
||||||
|
SDL_Renderer *r = g_engine.renderer;
|
||||||
|
|
||||||
|
/* Dark overlay */
|
||||||
|
SDL_SetRenderDrawBlendMode(r, SDL_BLENDMODE_BLEND);
|
||||||
|
SDL_SetRenderDrawColor(r, 0, 0, 0, 180);
|
||||||
|
SDL_Rect overlay = {0, 0, SCREEN_WIDTH, SCREEN_HEIGHT};
|
||||||
|
SDL_RenderFillRect(r, &overlay);
|
||||||
|
|
||||||
|
SDL_Color col_title = {220, 50, 50, 255};
|
||||||
|
SDL_Color col_text = {200, 200, 200, 255};
|
||||||
|
SDL_Color col_score = {255, 220, 80, 255};
|
||||||
|
SDL_Color col_dim = {120, 120, 130, 255};
|
||||||
|
|
||||||
|
int cy = SCREEN_HEIGHT / 2 - 50;
|
||||||
|
|
||||||
|
font_draw_text_centered(r, "GAME OVER", cy, SCREEN_WIDTH, col_title);
|
||||||
|
cy += 20;
|
||||||
|
|
||||||
|
/* Show final score */
|
||||||
|
{
|
||||||
|
char score_buf[32];
|
||||||
|
snprintf(score_buf, sizeof(score_buf), "SCORE: %d", s_stats.score);
|
||||||
|
font_draw_text_centered(r, score_buf, cy, SCREEN_WIDTH, col_score);
|
||||||
|
cy += 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!s_go_submitted) {
|
||||||
|
font_draw_text_centered(r, "ENTER YOUR NAME", cy, SCREEN_WIDTH, col_text);
|
||||||
|
cy += 16;
|
||||||
|
|
||||||
|
/* Draw name characters with cursor highlight */
|
||||||
|
{
|
||||||
|
char name_display[16];
|
||||||
|
snprintf(name_display, sizeof(name_display), "%c %c %c",
|
||||||
|
s_go_name[0], s_go_name[1], s_go_name[2]);
|
||||||
|
int nw = font_text_width(name_display);
|
||||||
|
int nx = (SCREEN_WIDTH - nw) / 2;
|
||||||
|
int spacing = nw / 3;
|
||||||
|
|
||||||
|
for (int i = 0; i < NAME_MAX_LEN; i++) {
|
||||||
|
char ch[2] = { s_go_name[i], '\0' };
|
||||||
|
SDL_Color c = (i == s_go_cursor) ? col_score : col_text;
|
||||||
|
int cx = nx + i * spacing;
|
||||||
|
font_draw_text(r, ch, cx, cy, c);
|
||||||
|
|
||||||
|
/* Draw cursor arrow above selected character */
|
||||||
|
if (i == s_go_cursor) {
|
||||||
|
font_draw_text(r, "^", cx, cy + 10, col_score);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cy += 24;
|
||||||
|
}
|
||||||
|
|
||||||
|
font_draw_text_centered(r, "UP/DOWN=LETTER LEFT/RIGHT=MOVE",
|
||||||
|
cy, SCREEN_WIDTH, col_dim);
|
||||||
|
cy += 12;
|
||||||
|
font_draw_text_centered(r, "JUMP=CONFIRM", cy, SCREEN_WIDTH, col_dim);
|
||||||
|
} else {
|
||||||
|
/* Name submitted — show confirmation */
|
||||||
|
{
|
||||||
|
char msg[32];
|
||||||
|
snprintf(msg, sizeof(msg), "NAME: %s", s_go_name);
|
||||||
|
font_draw_text_centered(r, msg, cy, SCREEN_WIDTH, col_text);
|
||||||
|
cy += 20;
|
||||||
|
}
|
||||||
|
font_draw_text_centered(r, "PRESS JUMP TO CONTINUE",
|
||||||
|
cy, SCREEN_WIDTH, col_dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_SetRenderDrawBlendMode(r, SDL_BLENDMODE_NONE);
|
||||||
|
}
|
||||||
|
|
||||||
static void game_render(float interpolation) {
|
static void game_render(float interpolation) {
|
||||||
if (s_mode == MODE_EDITOR) {
|
if (s_mode == MODE_EDITOR) {
|
||||||
editor_render(&s_editor, interpolation);
|
editor_render(&s_editor, interpolation);
|
||||||
@@ -658,20 +832,25 @@ static void game_render(float interpolation) {
|
|||||||
/* Render the level (frozen) with the transition overlay on top. */
|
/* Render the level (frozen) with the transition overlay on top. */
|
||||||
level_render(&s_level, interpolation);
|
level_render(&s_level, interpolation);
|
||||||
transition_render(&s_transition);
|
transition_render(&s_transition);
|
||||||
|
} else if (s_mode == MODE_GAMEOVER) {
|
||||||
|
/* Render frozen game frame with game over overlay. */
|
||||||
|
level_render(&s_level, interpolation);
|
||||||
|
gameover_render();
|
||||||
} else {
|
} else {
|
||||||
level_render(&s_level, interpolation);
|
level_render(&s_level, interpolation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void game_shutdown(void) {
|
static void game_shutdown(void) {
|
||||||
end_session("quit");
|
end_session("quit", NULL);
|
||||||
debuglog_set_level(NULL, NULL);
|
debuglog_set_level(NULL, NULL);
|
||||||
|
|
||||||
/* Always free both — editor may have been initialized even if we're
|
/* Always free both — editor may have been initialized even if we're
|
||||||
* currently in play mode (e.g. shutdown during test play). editor_free
|
* currently in play mode (e.g. shutdown during test play). editor_free
|
||||||
* and level_free are safe to call on zeroed/already-freed structs. */
|
* and level_free are safe to call on zeroed/already-freed structs. */
|
||||||
if (s_mode == MODE_PLAY || s_mode == MODE_PAUSED
|
if (s_mode == MODE_PLAY || s_mode == MODE_PAUSED
|
||||||
|| s_mode == MODE_TRANSITION || s_testing_from_editor) {
|
|| s_mode == MODE_TRANSITION || s_mode == MODE_GAMEOVER
|
||||||
|
|| s_testing_from_editor) {
|
||||||
level_free(&s_level);
|
level_free(&s_level);
|
||||||
}
|
}
|
||||||
if (s_mode == MODE_EDITOR || s_use_editor) {
|
if (s_mode == MODE_EDITOR || s_use_editor) {
|
||||||
|
|||||||
Reference in New Issue
Block a user