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:
Thomas
2026-02-28 20:24:43 +00:00
parent c66c12ae68
commit ea6e16358f
30 changed files with 4959 additions and 51 deletions

View File

@@ -2,7 +2,9 @@
#include "game/player.h"
#include "game/enemy.h"
#include "game/projectile.h"
#include "game/hazards.h"
#include "game/sprites.h"
#include "game/entity_registry.h"
#include "engine/core.h"
#include "engine/renderer.h"
#include "engine/physics.h"
@@ -19,9 +21,8 @@ static Sound s_sfx_hit;
static Sound s_sfx_enemy_death;
static bool s_sfx_loaded = false;
bool level_load(Level *level, const char *path) {
memset(level, 0, sizeof(Level));
/* ── Shared level setup (after tilemap is ready) ─── */
static bool level_setup(Level *level) {
/* Initialize subsystems */
entity_manager_init(&level->entities);
camera_init(&level->camera, SCREEN_WIDTH, SCREEN_HEIGHT);
@@ -40,17 +41,8 @@ bool level_load(Level *level, const char *path) {
fprintf(stderr, "Warning: failed to generate spritesheet\n");
}
/* Register entity types */
player_register(&level->entities);
player_set_entity_manager(&level->entities);
grunt_register(&level->entities);
flyer_register(&level->entities);
projectile_register(&level->entities);
/* Load tilemap */
if (!tilemap_load(&level->map, path, g_engine.renderer)) {
return false;
}
/* Register all entity types via the central registry */
entity_registry_init(&level->entities);
/* Apply level gravity (0 = use default) */
if (level->map.gravity > 0) {
@@ -75,11 +67,19 @@ bool level_load(Level *level, const char *path) {
if (near_tex) parallax_set_near(&level->parallax, near_tex, 0.15f, 0.10f);
}
/* Generate procedural backgrounds for any layers not loaded from file */
if (!level->parallax.far_layer.active) {
parallax_generate_stars(&level->parallax, g_engine.renderer);
}
if (!level->parallax.near_layer.active) {
parallax_generate_nebula(&level->parallax, g_engine.renderer);
if (!level->parallax.far_layer.active && !level->parallax.near_layer.active
&& level->map.parallax_style != 0) {
/* Use themed parallax when a style is specified */
parallax_generate_themed(&level->parallax, g_engine.renderer,
(ParallaxStyle)level->map.parallax_style);
} else {
/* Default: generic stars + nebula */
if (!level->parallax.far_layer.active) {
parallax_generate_stars(&level->parallax, g_engine.renderer);
}
if (!level->parallax.near_layer.active) {
parallax_generate_nebula(&level->parallax, g_engine.renderer);
}
}
/* Set camera bounds to level size */
@@ -94,18 +94,11 @@ bool level_load(Level *level, const char *path) {
return false;
}
/* Spawn entities from level data */
/* Spawn entities from level data (via registry) */
for (int i = 0; i < level->map.entity_spawn_count; i++) {
EntitySpawn *es = &level->map.entity_spawns[i];
Vec2 pos = vec2(es->x, es->y);
if (strcmp(es->type_name, "grunt") == 0) {
grunt_spawn(&level->entities, pos);
} else if (strcmp(es->type_name, "flyer") == 0) {
flyer_spawn(&level->entities, pos);
} else {
fprintf(stderr, "Unknown entity type: %s\n", es->type_name);
}
entity_registry_spawn(&level->entities, es->type_name, pos);
}
/* Load level music (playback deferred to first update —
@@ -119,6 +112,35 @@ bool level_load(Level *level, const char *path) {
return true;
}
bool level_load(Level *level, const char *path) {
memset(level, 0, sizeof(Level));
/* Load tilemap from file */
if (!tilemap_load(&level->map, path, g_engine.renderer)) {
return false;
}
return level_setup(level);
}
bool level_load_generated(Level *level, Tilemap *gen_map) {
memset(level, 0, sizeof(Level));
/* Take ownership of the generated tilemap */
level->map = *gen_map;
memset(gen_map, 0, sizeof(Tilemap)); /* prevent double-free */
/* Load tileset texture (the generator doesn't do this) */
level->map.tileset = assets_get_texture("assets/tiles/tileset.png");
if (level->map.tileset) {
int tex_w;
SDL_QueryTexture(level->map.tileset, NULL, NULL, &tex_w, NULL);
level->map.tileset_cols = tex_w / TILE_SIZE;
}
return level_setup(level);
}
/* ── Collision handling ──────────────────────────── */
/* Forward declaration for shake access */
@@ -139,6 +161,8 @@ static void damage_entity(Entity *target, int damage) {
death_color = (SDL_Color){200, 60, 60, 255}; /* red debris */
} else if (target->type == ENT_ENEMY_FLYER) {
death_color = (SDL_Color){140, 80, 200, 255}; /* purple puff */
} else if (target->type == ENT_TURRET) {
death_color = (SDL_Color){160, 160, 160, 255}; /* metal scraps */
} else {
death_color = (SDL_Color){200, 200, 200, 255}; /* grey */
}
@@ -179,7 +203,8 @@ static void damage_player(Entity *player, int damage, Entity *source) {
}
static bool is_enemy(const Entity *e) {
return e->type == ENT_ENEMY_GRUNT || e->type == ENT_ENEMY_FLYER;
return e->type == ENT_ENEMY_GRUNT || e->type == ENT_ENEMY_FLYER ||
e->type == ENT_TURRET;
}
static void handle_collisions(EntityManager *em) {
@@ -392,6 +417,11 @@ void level_render(Level *level, float interpolation) {
void level_free(Level *level) {
audio_stop_music();
/* Free music handle (prevent leak on reload) */
audio_free_music(&level->music);
level->music_started = false;
entity_manager_clear(&level->entities);
particle_clear();
parallax_free(&level->parallax);