Add in-game level editor with auto-discovered tile/entity palettes
Implements a full level editor that runs inside the game engine as an alternative mode, accessible via --edit flag or E key during gameplay. The editor auto-discovers available tiles from the tileset texture and entities from a new central registry, so adding new game content automatically appears in the editor without any editor-specific changes. Editor features: tile painting (pencil/eraser/flood fill) across 3 layers, entity placement with drag-to-move, player spawn point tool, camera pan/zoom, grid overlay, .lvl save/load, map resize, and test play (P to play, ESC to return to editor). Supporting changes: - Entity registry centralizes spawn functions (replaces strcmp chain) - Mouse input + raw keyboard access added to input system - Camera zoom support for editor overview - Zoom-aware rendering in tilemap, renderer, and sprite systems - Powerup and drone sprites/animations wired up (were defined but unused) - Bitmap font renderer for editor UI (4x6 pixel glyphs, no dependencies)
This commit is contained in:
278
src/main.c
278
src/main.c
@@ -1,38 +1,298 @@
|
||||
#include "engine/core.h"
|
||||
#include "engine/input.h"
|
||||
#include "game/level.h"
|
||||
#include "game/levelgen.h"
|
||||
#include "game/editor.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <SDL2/SDL.h>
|
||||
|
||||
static Level s_level;
|
||||
/* ═══════════════════════════════════════════════════
|
||||
* Game modes
|
||||
* ═══════════════════════════════════════════════════ */
|
||||
|
||||
static void game_init(void) {
|
||||
if (!level_load(&s_level, "assets/levels/level01.lvl")) {
|
||||
fprintf(stderr, "Failed to load level!\n");
|
||||
typedef enum GameMode {
|
||||
MODE_PLAY,
|
||||
MODE_EDITOR,
|
||||
} 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};
|
||||
|
||||
/* Track whether we came from the editor (for returning after test play) */
|
||||
static bool s_testing_from_editor = false;
|
||||
|
||||
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";
|
||||
default: return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
static void load_generated_level(void) {
|
||||
LevelGenConfig config = levelgen_default_config();
|
||||
config.seed = s_gen_seed;
|
||||
config.num_segments = 6;
|
||||
config.difficulty = 0.5f;
|
||||
|
||||
/* 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);
|
||||
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: /* Single theme (derived from seed) */
|
||||
default: {
|
||||
LevelTheme single = (LevelTheme)(seed_for_theme / 4 % 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;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── 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");
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════
|
||||
* Game callbacks
|
||||
* ═══════════════════════════════════════════════════ */
|
||||
|
||||
static void game_init(void) {
|
||||
if (s_use_editor) {
|
||||
enter_editor();
|
||||
} else if (s_use_procgen) {
|
||||
load_generated_level();
|
||||
} else {
|
||||
if (!level_load(&s_level, "assets/levels/level01.lvl")) {
|
||||
fprintf(stderr, "Failed to load level!\n");
|
||||
g_engine.running = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void game_update(float dt) {
|
||||
/* Quit on escape */
|
||||
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;
|
||||
}
|
||||
|
||||
/* ── Play mode ── */
|
||||
|
||||
/* Quit / return to editor on escape */
|
||||
if (input_pressed(ACTION_PAUSE)) {
|
||||
if (s_testing_from_editor) {
|
||||
return_to_editor();
|
||||
return;
|
||||
}
|
||||
g_engine.running = false;
|
||||
return;
|
||||
}
|
||||
|
||||
/* E key: enter editor from gameplay (not during test play) */
|
||||
if (!s_testing_from_editor && input_key_pressed(SDL_SCANCODE_E)) {
|
||||
/* Save current level path for potential re-editing */
|
||||
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");
|
||||
level_free(&s_level);
|
||||
s_gen_seed = (uint32_t)time(NULL);
|
||||
s_use_procgen = true;
|
||||
load_generated_level();
|
||||
}
|
||||
r_was_pressed = r_pressed;
|
||||
|
||||
level_update(&s_level, dt);
|
||||
}
|
||||
|
||||
static void game_render(float interpolation) {
|
||||
level_render(&s_level, interpolation);
|
||||
if (s_mode == MODE_EDITOR) {
|
||||
editor_render(&s_editor, interpolation);
|
||||
} else {
|
||||
level_render(&s_level, interpolation);
|
||||
}
|
||||
}
|
||||
|
||||
static void game_shutdown(void) {
|
||||
level_free(&s_level);
|
||||
/* 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) {
|
||||
level_free(&s_level);
|
||||
}
|
||||
if (s_mode == MODE_EDITOR || s_use_editor) {
|
||||
editor_free(&s_editor);
|
||||
}
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════
|
||||
* Main
|
||||
* ═══════════════════════════════════════════════════ */
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
(void)argc;
|
||||
(void)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 Quit (or return to editor from test play)\n");
|
||||
printf("\nEditor:\n");
|
||||
printf(" 1-5 Select tool (Pencil/Eraser/Fill/Entity/Spawn)\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++/- Resize level width\n");
|
||||
printf(" P Test play level\n");
|
||||
printf(" ESC Quit editor\n");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
srand((unsigned)time(NULL));
|
||||
|
||||
if (!engine_init()) {
|
||||
return 1;
|
||||
|
||||
Reference in New Issue
Block a user