744 lines
26 KiB
C
744 lines
26 KiB
C
#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 "game/stats.h"
|
|
#include "game/analytics.h"
|
|
#include "game/transition.h"
|
|
#include "config.h"
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
#include <SDL2/SDL.h>
|
|
|
|
#ifdef __EMSCRIPTEN__
|
|
#include <emscripten.h>
|
|
#endif
|
|
|
|
/* ═══════════════════════════════════════════════════
|
|
* Game modes
|
|
* ═══════════════════════════════════════════════════ */
|
|
|
|
typedef enum GameMode {
|
|
MODE_PLAY,
|
|
MODE_EDITOR,
|
|
MODE_PAUSED,
|
|
MODE_TRANSITION,
|
|
} GameMode;
|
|
|
|
static Level s_level;
|
|
static Editor s_editor;
|
|
static GameMode s_mode = MODE_PLAY;
|
|
static bool s_use_procgen = false;
|
|
static bool s_dump_lvl = false;
|
|
static bool s_use_editor = false;
|
|
static uint32_t s_gen_seed = 0;
|
|
static char s_edit_path[256] = {0};
|
|
static char s_level_path[ASSET_PATH_MAX] = {0}; /* path of active play-mode level */
|
|
|
|
/* Track whether we came from the editor (for returning after test play) */
|
|
static bool s_testing_from_editor = false;
|
|
|
|
/* Station depth: increments each time we enter a new station level.
|
|
* Drives escalating difficulty and length. */
|
|
static int s_station_depth = 0;
|
|
|
|
/* Mars Base depth: increments each generated mars_base level.
|
|
* After 2 levels, transitions to the boss arena (mars03). */
|
|
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 */
|
|
|
|
/* ── Level transition state ── */
|
|
static TransitionState s_transition;
|
|
static char s_pending_target[ASSET_PATH_MAX] = {0}; /* exit target stashed during transition */
|
|
|
|
#ifdef __EMSCRIPTEN__
|
|
/* JS-initiated level load request (level-select dropdown in shell). */
|
|
static int s_js_load_request = 0;
|
|
static char s_js_load_path[ASSET_PATH_MAX] = {0};
|
|
#endif
|
|
|
|
#ifdef __EMSCRIPTEN__
|
|
/* Called from the JS shell level-select dropdown to load a level into
|
|
* gameplay mode. Sets a deferred request that game_update() picks up on
|
|
* the next frame so we don't mutate game state from an arbitrary call site. */
|
|
EMSCRIPTEN_KEEPALIVE
|
|
void game_load_level(const char *path) {
|
|
snprintf(s_js_load_path, sizeof(s_js_load_path), "%s", path);
|
|
s_js_load_request = 1;
|
|
}
|
|
#endif
|
|
|
|
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";
|
|
}
|
|
}
|
|
|
|
/* Load a file-based level and remember its path for editor access. */
|
|
static bool load_level_file(const char *path) {
|
|
if (!level_load(&s_level, path)) return false;
|
|
snprintf(s_level_path, sizeof(s_level_path), "%s", path);
|
|
return true;
|
|
}
|
|
|
|
static void load_generated_level(void) {
|
|
LevelGenConfig config = levelgen_default_config();
|
|
config.seed = s_gen_seed;
|
|
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;
|
|
int r = (int)(seed_for_theme % 6);
|
|
switch (r) {
|
|
case 0: /* Surface -> Base */
|
|
config.themes[0] = THEME_PLANET_SURFACE;
|
|
config.themes[1] = THEME_PLANET_SURFACE;
|
|
config.themes[2] = THEME_PLANET_BASE;
|
|
config.themes[3] = THEME_PLANET_BASE;
|
|
config.themes[4] = THEME_PLANET_BASE;
|
|
config.themes[5] = THEME_PLANET_BASE;
|
|
config.theme_count = 6;
|
|
break;
|
|
case 1: /* Base -> Station */
|
|
config.themes[0] = THEME_PLANET_BASE;
|
|
config.themes[1] = THEME_PLANET_BASE;
|
|
config.themes[2] = THEME_PLANET_BASE;
|
|
config.themes[3] = THEME_SPACE_STATION;
|
|
config.themes[4] = THEME_SPACE_STATION;
|
|
config.themes[5] = THEME_SPACE_STATION;
|
|
config.theme_count = 6;
|
|
break;
|
|
case 2: /* Surface -> Base -> Station (full journey) */
|
|
config.themes[0] = THEME_PLANET_SURFACE;
|
|
config.themes[1] = THEME_PLANET_SURFACE;
|
|
config.themes[2] = THEME_PLANET_BASE;
|
|
config.themes[3] = THEME_PLANET_BASE;
|
|
config.themes[4] = THEME_SPACE_STATION;
|
|
config.themes[5] = THEME_SPACE_STATION;
|
|
config.theme_count = 6;
|
|
break;
|
|
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 / 6 % THEME_COUNT);
|
|
config.themes[0] = single;
|
|
config.theme_count = 1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
printf("Theme sequence:");
|
|
for (int i = 0; i < config.theme_count; i++) {
|
|
printf(" %s", theme_name(config.themes[i]));
|
|
}
|
|
printf("\n");
|
|
|
|
Tilemap gen_map;
|
|
if (!levelgen_generate(&gen_map, &config)) {
|
|
fprintf(stderr, "Failed to generate level!\n");
|
|
g_engine.running = false;
|
|
return;
|
|
}
|
|
|
|
/* Optionally dump to file for inspection */
|
|
if (s_dump_lvl) {
|
|
levelgen_dump_lvl(&gen_map, "assets/levels/generated.lvl");
|
|
}
|
|
|
|
if (!level_load_generated(&s_level, &gen_map)) {
|
|
fprintf(stderr, "Failed to load generated level!\n");
|
|
g_engine.running = false;
|
|
}
|
|
s_level_path[0] = '\0'; /* generated levels have no file path */
|
|
}
|
|
|
|
static void load_station_level(void) {
|
|
LevelGenConfig config = levelgen_station_config(s_gen_seed, s_station_depth);
|
|
s_station_depth++;
|
|
|
|
printf("Generating space station level (depth=%d, gravity=%.0f, segments=%d, difficulty=%.2f)\n",
|
|
s_station_depth, config.gravity, config.num_segments, config.difficulty);
|
|
|
|
Tilemap gen_map;
|
|
if (!levelgen_generate_station(&gen_map, &config)) {
|
|
fprintf(stderr, "Failed to generate station level!\n");
|
|
g_engine.running = false;
|
|
return;
|
|
}
|
|
|
|
if (s_dump_lvl) {
|
|
levelgen_dump_lvl(&gen_map, "assets/levels/generated_station.lvl");
|
|
}
|
|
|
|
if (!level_load_generated(&s_level, &gen_map)) {
|
|
fprintf(stderr, "Failed to load station level!\n");
|
|
g_engine.running = false;
|
|
}
|
|
s_level_path[0] = '\0'; /* generated levels have no file path */
|
|
}
|
|
|
|
static void load_mars_base_level(void) {
|
|
LevelGenConfig config = levelgen_mars_base_config(s_gen_seed, s_mars_depth);
|
|
s_mars_depth++;
|
|
|
|
printf("Generating Mars Base level (depth=%d, gravity=%.0f, segments=%d, difficulty=%.2f)\n",
|
|
s_mars_depth, config.gravity, config.num_segments, config.difficulty);
|
|
|
|
Tilemap gen_map;
|
|
if (!levelgen_generate_mars_base(&gen_map, &config)) {
|
|
fprintf(stderr, "Failed to generate Mars Base level!\n");
|
|
g_engine.running = false;
|
|
return;
|
|
}
|
|
|
|
/* After MARS_BASE_GEN_COUNT generated levels, point exit to boss arena */
|
|
if (s_mars_depth >= MARS_BASE_GEN_COUNT && gen_map.exit_zone_count > 0) {
|
|
ExitZone *ez = &gen_map.exit_zones[gen_map.exit_zone_count - 1];
|
|
snprintf(ez->target, sizeof(ez->target), "assets/levels/mars03.lvl");
|
|
}
|
|
|
|
if (s_dump_lvl) {
|
|
levelgen_dump_lvl(&gen_map, "assets/levels/generated_mars_base.lvl");
|
|
}
|
|
|
|
if (!level_load_generated(&s_level, &gen_map)) {
|
|
fprintf(stderr, "Failed to load Mars Base level!\n");
|
|
g_engine.running = false;
|
|
}
|
|
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) {
|
|
level_free(&s_level);
|
|
}
|
|
s_mode = MODE_EDITOR;
|
|
|
|
editor_init(&s_editor);
|
|
if (s_edit_path[0]) {
|
|
if (!editor_load(&s_editor, s_edit_path)) {
|
|
fprintf(stderr, "Failed to load %s, creating new level\n", s_edit_path);
|
|
editor_new_level(&s_editor, 40, 23);
|
|
}
|
|
} else {
|
|
editor_new_level(&s_editor, 40, 23);
|
|
}
|
|
|
|
/* Change window title */
|
|
SDL_SetWindowTitle(g_engine.window, "Jump 'n Run - Level Editor");
|
|
}
|
|
|
|
/* ── Switch to play mode (test play from editor) ── */
|
|
static void enter_test_play(void) {
|
|
/* Save the current level to a temp file */
|
|
editor_save_as(&s_editor, "assets/levels/_editor_test.lvl");
|
|
|
|
s_mode = MODE_PLAY;
|
|
s_testing_from_editor = true;
|
|
|
|
if (!level_load(&s_level, "assets/levels/_editor_test.lvl")) {
|
|
fprintf(stderr, "Failed to load editor test level!\n");
|
|
/* Fall back to editor */
|
|
s_mode = MODE_EDITOR;
|
|
s_testing_from_editor = false;
|
|
return;
|
|
}
|
|
|
|
SDL_SetWindowTitle(g_engine.window, "Jump 'n Run - Testing");
|
|
}
|
|
|
|
/* ── Return from test play to editor ── */
|
|
static void return_to_editor(void) {
|
|
level_free(&s_level);
|
|
s_mode = MODE_EDITOR;
|
|
s_testing_from_editor = false;
|
|
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();
|
|
}
|
|
}
|
|
|
|
/* ── Level load dispatch — loads the next level based on target string ── */
|
|
static void dispatch_level_load(const char *target) {
|
|
if (target[0] == '\0') {
|
|
/* Empty target = victory / end of game. */
|
|
printf("Level complete! (no next level)\n");
|
|
end_session("completed");
|
|
level_free(&s_level);
|
|
s_station_depth = 0;
|
|
s_mars_depth = 0;
|
|
if (!load_level_file("assets/levels/moon01.lvl")) {
|
|
g_engine.running = false;
|
|
}
|
|
begin_session();
|
|
} else if (strcmp(target, "generate") == 0) {
|
|
printf("Transitioning to generated level\n");
|
|
level_free(&s_level);
|
|
s_gen_seed = (uint32_t)time(NULL);
|
|
load_generated_level();
|
|
} else if (strcmp(target, "generate:station") == 0) {
|
|
printf("Transitioning to space station level\n");
|
|
level_free(&s_level);
|
|
s_gen_seed = (uint32_t)time(NULL);
|
|
load_station_level();
|
|
} else if (strcmp(target, "generate:mars_base") == 0) {
|
|
printf("Transitioning to Mars Base level\n");
|
|
level_free(&s_level);
|
|
s_gen_seed = (uint32_t)time(NULL);
|
|
load_mars_base_level();
|
|
} else {
|
|
printf("Transitioning to: %s\n", target);
|
|
char path[ASSET_PATH_MAX];
|
|
snprintf(path, sizeof(path), "%s", target);
|
|
level_free(&s_level);
|
|
if (!load_level_file(path)) {
|
|
fprintf(stderr, "Failed to load next level: %s\n", path);
|
|
if (!load_level_file("assets/levels/moon01.lvl")) {
|
|
g_engine.running = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════
|
|
* Game callbacks
|
|
* ═══════════════════════════════════════════════════ */
|
|
|
|
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();
|
|
}
|
|
}
|
|
|
|
/* ── 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;
|
|
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;
|
|
}
|
|
}
|
|
|
|
static void game_update(float dt) {
|
|
#ifdef __EMSCRIPTEN__
|
|
/* 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
|
|
|| s_mode == MODE_TRANSITION) {
|
|
transition_reset(&s_transition);
|
|
level_free(&s_level);
|
|
} else if (s_mode == MODE_EDITOR) {
|
|
editor_free(&s_editor);
|
|
}
|
|
|
|
s_mode = MODE_PLAY;
|
|
s_testing_from_editor = false;
|
|
|
|
if (!load_level_file(s_js_load_path)) {
|
|
fprintf(stderr, "Failed to load level from shell: %s\n",
|
|
s_js_load_path);
|
|
/* Fall back to the first campaign level. */
|
|
if (!load_level_file("assets/levels/moon01.lvl")) {
|
|
g_engine.running = false;
|
|
}
|
|
}
|
|
|
|
/* Also seed the editor path so pressing E opens this level. */
|
|
snprintf(s_edit_path, sizeof(s_edit_path), "%s", s_js_load_path);
|
|
s_js_load_path[0] = '\0';
|
|
|
|
SDL_SetWindowTitle(g_engine.window, "Jump 'n Run");
|
|
begin_session();
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
if (s_mode == MODE_EDITOR) {
|
|
editor_update(&s_editor, dt);
|
|
|
|
if (editor_wants_test_play(&s_editor)) {
|
|
enter_test_play();
|
|
}
|
|
if (editor_wants_quit(&s_editor)) {
|
|
g_engine.running = false;
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (s_mode == MODE_PAUSED) {
|
|
pause_update();
|
|
return;
|
|
}
|
|
|
|
if (s_mode == MODE_TRANSITION) {
|
|
transition_update(&s_transition, dt, &s_level.camera);
|
|
|
|
/* Outro finished — swap levels. */
|
|
if (transition_needs_load(&s_transition)) {
|
|
dispatch_level_load(s_pending_target);
|
|
s_pending_target[0] = '\0';
|
|
|
|
/* Use the new level's intro style. */
|
|
TransitionStyle in_style = s_level.map.transition_in;
|
|
transition_set_in_style(&s_transition, in_style);
|
|
transition_begin_intro(&s_transition);
|
|
}
|
|
|
|
/* Intro finished — return to play. */
|
|
if (transition_is_done(&s_transition)) {
|
|
transition_reset(&s_transition);
|
|
s_mode = MODE_PLAY;
|
|
}
|
|
return;
|
|
}
|
|
|
|
/* ── Play mode ── */
|
|
|
|
/* Pause on escape (return to editor during test play) */
|
|
if (input_pressed(ACTION_PAUSE)) {
|
|
if (s_testing_from_editor) {
|
|
return_to_editor();
|
|
return;
|
|
}
|
|
s_pause_selection = 0;
|
|
s_mode = MODE_PAUSED;
|
|
return;
|
|
}
|
|
|
|
/* E key: enter editor from gameplay (not during test play) */
|
|
if (!s_testing_from_editor && input_key_pressed(SDL_SCANCODE_E)) {
|
|
/* Load the current level file into the editor if available */
|
|
snprintf(s_edit_path, sizeof(s_edit_path), "%s", s_level_path);
|
|
level_free(&s_level);
|
|
enter_editor();
|
|
return;
|
|
}
|
|
|
|
/* R key: regenerate level with new seed */
|
|
static bool r_was_pressed = false;
|
|
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++;
|
|
}
|
|
|
|
TransitionStyle out_style = s_level.map.transition_out;
|
|
|
|
if (out_style == TRANS_ELEVATOR || out_style == TRANS_TELEPORTER) {
|
|
/* Animated transition: stash target, start outro. */
|
|
snprintf(s_pending_target, sizeof(s_pending_target), "%s", target);
|
|
transition_start_out(&s_transition, out_style);
|
|
s_mode = MODE_TRANSITION;
|
|
} else {
|
|
/* Instant transition (none or spacecraft-driven). */
|
|
dispatch_level_load(target);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ── 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 if (s_mode == MODE_TRANSITION) {
|
|
/* Render the level (frozen) with the transition overlay on top. */
|
|
level_render(&s_level, interpolation);
|
|
transition_render(&s_transition);
|
|
} else {
|
|
level_render(&s_level, 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. */
|
|
if (s_mode == MODE_PLAY || s_mode == MODE_PAUSED
|
|
|| s_mode == MODE_TRANSITION || s_testing_from_editor) {
|
|
level_free(&s_level);
|
|
}
|
|
if (s_mode == MODE_EDITOR || s_use_editor) {
|
|
editor_free(&s_editor);
|
|
}
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════
|
|
* Main
|
|
* ═══════════════════════════════════════════════════ */
|
|
|
|
int main(int argc, char *argv[]) {
|
|
/* Parse command-line arguments */
|
|
for (int i = 1; i < argc; i++) {
|
|
if (strcmp(argv[i], "--generate") == 0 || strcmp(argv[i], "-g") == 0) {
|
|
s_use_procgen = true;
|
|
} else if (strcmp(argv[i], "--dump") == 0 || strcmp(argv[i], "-d") == 0) {
|
|
s_dump_lvl = true;
|
|
} else if (strcmp(argv[i], "--seed") == 0 || strcmp(argv[i], "-s") == 0) {
|
|
if (i + 1 < argc) {
|
|
s_gen_seed = (uint32_t)atoi(argv[++i]);
|
|
}
|
|
} else if (strcmp(argv[i], "--edit") == 0 || strcmp(argv[i], "-e") == 0) {
|
|
s_use_editor = true;
|
|
/* Optional: next arg is a file path */
|
|
if (i + 1 < argc && argv[i + 1][0] != '-') {
|
|
strncpy(s_edit_path, argv[++i], sizeof(s_edit_path) - 1);
|
|
}
|
|
} else if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) {
|
|
printf("Usage: jnr [options]\n");
|
|
printf(" --generate, -g Load a procedurally generated level\n");
|
|
printf(" --dump, -d Dump generated level to assets/levels/generated.lvl\n");
|
|
printf(" --seed N, -s N Set RNG seed for generation\n");
|
|
printf(" --edit [file], -e [file] Open level editor (optionally load a .lvl file)\n");
|
|
printf("\nIn-game:\n");
|
|
printf(" R Regenerate level with new random seed\n");
|
|
printf(" E Open level editor\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");
|
|
printf(" G Toggle grid\n");
|
|
printf(" V Toggle all-layer visibility\n");
|
|
printf(" Arrow keys / WASD Pan camera\n");
|
|
printf(" Scroll wheel Zoom in/out\n");
|
|
printf(" Middle mouse drag Pan camera\n");
|
|
printf(" Left click Paint/place (canvas) or select (palette)\n");
|
|
printf(" Right click Pick tile / delete entity\n");
|
|
printf(" Ctrl+S Save level\n");
|
|
printf(" Ctrl+O Open/load level\n");
|
|
printf(" Ctrl++/- Resize level width\n");
|
|
printf(" P Test play level\n");
|
|
printf(" ESC Quit editor\n");
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
srand((unsigned)time(NULL));
|
|
|
|
#ifdef __EMSCRIPTEN__
|
|
/* Check URL query string for ?edit or ?edit=filename */
|
|
{
|
|
const char *qs = emscripten_run_script_string(
|
|
"window.location.search || ''");
|
|
if (qs && strstr(qs, "edit")) {
|
|
s_use_editor = true;
|
|
/* Check for ?edit=filename */
|
|
const char *eq = strstr(qs, "edit=");
|
|
if (eq) {
|
|
eq += 5; /* skip "edit=" */
|
|
/* Copy until & or end of string */
|
|
int len = 0;
|
|
while (eq[len] && eq[len] != '&' && len < (int)sizeof(s_edit_path) - 1) {
|
|
s_edit_path[len] = eq[len];
|
|
len++;
|
|
}
|
|
s_edit_path[len] = '\0';
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
if (!engine_init()) {
|
|
return 1;
|
|
}
|
|
|
|
engine_set_callbacks((GameCallbacks){
|
|
.init = game_init,
|
|
.update = game_update,
|
|
.render = game_render,
|
|
.shutdown = game_shutdown,
|
|
});
|
|
|
|
engine_run();
|
|
engine_shutdown();
|
|
|
|
return 0;
|
|
}
|