Add pause menu, laser turret, charger/spawner enemies, and Mars campaign
Implement four feature phases: Phase 1 - Pause menu: extract bitmap font into shared engine/font module, add MODE_PAUSED with Resume/Restart/Quit overlay. Phase 2 - Laser turret hazard: ENT_LASER_TURRET with charge/fire/ cooldown state machine, per-pixel beam raycast, two variants (fixed and tracking). Registered in entity registry with editor icons. Phase 3 - Charger and Spawner enemies: charger ground patrol with detect/telegraph/charge/stun cycle (2s charge timeout), spawner that periodically creates grunts up to a global cap of 3. Phase 4 - Mars campaign: two handcrafted levels (mars01 surface, mars02 base), mars_tileset.png, PARALLAX_STYLE_MARS with salmon sky and red mesas, THEME_MARS_SURFACE/THEME_MARS_BASE for the procedural generator with per-theme gravity/tileset/parallax. Moon campaign now chains moon03 -> mars01 -> mars02 -> victory. Also fix review findings: deterministic seed on generated level restart, NULL checks on calloc in spawn functions, charge timeout to prevent infinite charge on flat terrain, and stop suppressing stderr in Makefile web-serve target so real errors are visible.
This commit is contained in:
156
src/main.c
156
src/main.c
@@ -1,8 +1,10 @@
|
||||
#include "engine/core.h"
|
||||
#include "engine/input.h"
|
||||
#include "engine/font.h"
|
||||
#include "game/level.h"
|
||||
#include "game/levelgen.h"
|
||||
#include "game/editor.h"
|
||||
#include "config.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
@@ -19,6 +21,7 @@
|
||||
typedef enum GameMode {
|
||||
MODE_PLAY,
|
||||
MODE_EDITOR,
|
||||
MODE_PAUSED,
|
||||
} GameMode;
|
||||
|
||||
static Level s_level;
|
||||
@@ -38,11 +41,17 @@ static bool s_testing_from_editor = false;
|
||||
* Drives escalating difficulty and length. */
|
||||
static int s_station_depth = 0;
|
||||
|
||||
/* ── Pause menu state ── */
|
||||
#define PAUSE_ITEM_COUNT 3
|
||||
static int s_pause_selection = 0; /* 0=Resume, 1=Restart, 2=Quit */
|
||||
|
||||
static const char *theme_name(LevelTheme t) {
|
||||
switch (t) {
|
||||
case THEME_PLANET_SURFACE: return "Planet Surface";
|
||||
case THEME_PLANET_BASE: return "Planet Base";
|
||||
case THEME_SPACE_STATION: return "Space Station";
|
||||
case THEME_MARS_SURFACE: return "Mars Surface";
|
||||
case THEME_MARS_BASE: return "Mars Base";
|
||||
default: return "Unknown";
|
||||
}
|
||||
}
|
||||
@@ -60,11 +69,17 @@ static void load_generated_level(void) {
|
||||
config.num_segments = 6;
|
||||
config.difficulty = 0.5f;
|
||||
|
||||
/* Ensure seed is non-zero so theme selection is deterministic
|
||||
* across restarts. If caller didn't set s_gen_seed (e.g. first
|
||||
* run with -gen but no -seed), snapshot time(NULL) now. */
|
||||
if (s_gen_seed == 0) s_gen_seed = (uint32_t)time(NULL);
|
||||
config.seed = s_gen_seed;
|
||||
|
||||
/* Build a theme progression — start on surface, move indoors/upward.
|
||||
* Derive from seed so it varies with each regeneration and doesn't
|
||||
* depend on rand() state (which parallax generators clobber). */
|
||||
uint32_t seed_for_theme = config.seed ? config.seed : (uint32_t)time(NULL);
|
||||
int r = (int)(seed_for_theme % 4);
|
||||
uint32_t seed_for_theme = config.seed;
|
||||
int r = (int)(seed_for_theme % 6);
|
||||
switch (r) {
|
||||
case 0: /* Surface -> Base */
|
||||
config.themes[0] = THEME_PLANET_SURFACE;
|
||||
@@ -93,9 +108,27 @@ static void load_generated_level(void) {
|
||||
config.themes[5] = THEME_SPACE_STATION;
|
||||
config.theme_count = 6;
|
||||
break;
|
||||
case 3: /* Single theme (derived from seed) */
|
||||
case 3: /* Mars Surface -> Mars Base */
|
||||
config.themes[0] = THEME_MARS_SURFACE;
|
||||
config.themes[1] = THEME_MARS_SURFACE;
|
||||
config.themes[2] = THEME_MARS_SURFACE;
|
||||
config.themes[3] = THEME_MARS_BASE;
|
||||
config.themes[4] = THEME_MARS_BASE;
|
||||
config.themes[5] = THEME_MARS_BASE;
|
||||
config.theme_count = 6;
|
||||
break;
|
||||
case 4: /* Mars Surface -> Mars Base -> Station */
|
||||
config.themes[0] = THEME_MARS_SURFACE;
|
||||
config.themes[1] = THEME_MARS_SURFACE;
|
||||
config.themes[2] = THEME_MARS_BASE;
|
||||
config.themes[3] = THEME_MARS_BASE;
|
||||
config.themes[4] = THEME_SPACE_STATION;
|
||||
config.themes[5] = THEME_SPACE_STATION;
|
||||
config.theme_count = 6;
|
||||
break;
|
||||
case 5: /* Single theme (derived from seed) */
|
||||
default: {
|
||||
LevelTheme single = (LevelTheme)(seed_for_theme / 4 % THEME_COUNT);
|
||||
LevelTheme single = (LevelTheme)(seed_for_theme / 6 % THEME_COUNT);
|
||||
config.themes[0] = single;
|
||||
config.theme_count = 1;
|
||||
break;
|
||||
@@ -200,6 +233,20 @@ static void return_to_editor(void) {
|
||||
SDL_SetWindowTitle(g_engine.window, "Jump 'n Run - Level Editor");
|
||||
}
|
||||
|
||||
/* ── Restart current level (file-based or generated) ── */
|
||||
static void restart_level(void) {
|
||||
level_free(&s_level);
|
||||
if (s_level_path[0]) {
|
||||
if (!load_level_file(s_level_path)) {
|
||||
fprintf(stderr, "Failed to restart level: %s\n", s_level_path);
|
||||
g_engine.running = false;
|
||||
}
|
||||
} else {
|
||||
/* Generated level — regenerate with same seed. */
|
||||
load_generated_level();
|
||||
}
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════
|
||||
* Game callbacks
|
||||
* ═══════════════════════════════════════════════════ */
|
||||
@@ -217,6 +264,48 @@ static void game_init(void) {
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Pause menu: handle input and confirm actions ── */
|
||||
static void pause_update(void) {
|
||||
/* Unpause on escape */
|
||||
if (input_pressed(ACTION_PAUSE)) {
|
||||
s_mode = MODE_PLAY;
|
||||
return;
|
||||
}
|
||||
|
||||
/* Navigate menu items */
|
||||
if (input_pressed(ACTION_UP)) {
|
||||
s_pause_selection--;
|
||||
if (s_pause_selection < 0) s_pause_selection = PAUSE_ITEM_COUNT - 1;
|
||||
}
|
||||
if (input_pressed(ACTION_DOWN)) {
|
||||
s_pause_selection++;
|
||||
if (s_pause_selection >= PAUSE_ITEM_COUNT) s_pause_selection = 0;
|
||||
}
|
||||
|
||||
/* Confirm selection with jump or enter */
|
||||
bool confirm = input_pressed(ACTION_JUMP)
|
||||
|| input_key_pressed(SDL_SCANCODE_RETURN)
|
||||
|| input_key_pressed(SDL_SCANCODE_RETURN2);
|
||||
if (!confirm) return;
|
||||
|
||||
switch (s_pause_selection) {
|
||||
case 0: /* Resume */
|
||||
s_mode = MODE_PLAY;
|
||||
break;
|
||||
case 1: /* Restart */
|
||||
s_mode = MODE_PLAY;
|
||||
restart_level();
|
||||
break;
|
||||
case 2: /* Quit */
|
||||
if (s_testing_from_editor) {
|
||||
return_to_editor();
|
||||
} else {
|
||||
g_engine.running = false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void game_update(float dt) {
|
||||
if (s_mode == MODE_EDITOR) {
|
||||
editor_update(&s_editor, dt);
|
||||
@@ -230,15 +319,21 @@ static void game_update(float dt) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (s_mode == MODE_PAUSED) {
|
||||
pause_update();
|
||||
return;
|
||||
}
|
||||
|
||||
/* ── Play mode ── */
|
||||
|
||||
/* Quit / return to editor on escape */
|
||||
/* Pause on escape (return to editor during test play) */
|
||||
if (input_pressed(ACTION_PAUSE)) {
|
||||
if (s_testing_from_editor) {
|
||||
return_to_editor();
|
||||
return;
|
||||
}
|
||||
g_engine.running = false;
|
||||
s_pause_selection = 0;
|
||||
s_mode = MODE_PAUSED;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -306,9 +401,54 @@ static void game_update(float dt) {
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Draw the pause menu overlay on top of the frozen game frame ── */
|
||||
static void pause_render(void) {
|
||||
SDL_Renderer *r = g_engine.renderer;
|
||||
|
||||
/* Semi-transparent dark overlay */
|
||||
SDL_SetRenderDrawBlendMode(r, SDL_BLENDMODE_BLEND);
|
||||
SDL_SetRenderDrawColor(r, 0, 0, 0, 140);
|
||||
SDL_Rect overlay = {0, 0, SCREEN_WIDTH, SCREEN_HEIGHT};
|
||||
SDL_RenderFillRect(r, &overlay);
|
||||
|
||||
/* Title */
|
||||
SDL_Color col_title = {255, 255, 255, 255};
|
||||
font_draw_text_centered(r, "PAUSED", SCREEN_HEIGHT / 2 - 40,
|
||||
SCREEN_WIDTH, col_title);
|
||||
|
||||
/* Menu items */
|
||||
static const char *items[PAUSE_ITEM_COUNT] = {
|
||||
"RESUME", "RESTART", "QUIT"
|
||||
};
|
||||
SDL_Color col_normal = {160, 160, 170, 255};
|
||||
SDL_Color col_active = {255, 220, 80, 255};
|
||||
|
||||
int item_y = SCREEN_HEIGHT / 2 - 8;
|
||||
for (int i = 0; i < PAUSE_ITEM_COUNT; i++) {
|
||||
SDL_Color c = (i == s_pause_selection) ? col_active : col_normal;
|
||||
|
||||
/* Draw selection indicator */
|
||||
if (i == s_pause_selection) {
|
||||
int tw = font_text_width(items[i]);
|
||||
int tx = (SCREEN_WIDTH - tw) / 2;
|
||||
font_draw_text(r, ">", tx - 8, item_y, col_active);
|
||||
}
|
||||
|
||||
font_draw_text_centered(r, items[i], item_y, SCREEN_WIDTH, c);
|
||||
item_y += 14;
|
||||
}
|
||||
|
||||
/* Restore blend mode */
|
||||
SDL_SetRenderDrawBlendMode(r, SDL_BLENDMODE_NONE);
|
||||
}
|
||||
|
||||
static void game_render(float interpolation) {
|
||||
if (s_mode == MODE_EDITOR) {
|
||||
editor_render(&s_editor, interpolation);
|
||||
} else if (s_mode == MODE_PAUSED) {
|
||||
/* Render frozen game frame, then overlay the pause menu. */
|
||||
level_render(&s_level, interpolation);
|
||||
pause_render();
|
||||
} else {
|
||||
level_render(&s_level, interpolation);
|
||||
}
|
||||
@@ -318,7 +458,7 @@ static void game_shutdown(void) {
|
||||
/* 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. */
|
||||
if (s_mode == MODE_PLAY || s_testing_from_editor) {
|
||||
if (s_mode == MODE_PLAY || s_mode == MODE_PAUSED || s_testing_from_editor) {
|
||||
level_free(&s_level);
|
||||
}
|
||||
if (s_mode == MODE_EDITOR || s_use_editor) {
|
||||
@@ -356,7 +496,7 @@ int main(int argc, char *argv[]) {
|
||||
printf("\nIn-game:\n");
|
||||
printf(" R Regenerate level with new random seed\n");
|
||||
printf(" E Open level editor\n");
|
||||
printf(" ESC Quit (or return to editor from test play)\n");
|
||||
printf(" ESC Pause (or return to editor from test play)\n");
|
||||
printf("\nEditor:\n");
|
||||
printf(" 1-6 Select tool (Pencil/Eraser/Fill/Entity/Spawn/Exit)\n");
|
||||
printf(" Q/W/E Select layer (Collision/BG/FG)\n");
|
||||
|
||||
Reference in New Issue
Block a user