#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 #include #include #include #ifdef __EMSCRIPTEN__ #include #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; }