forked from tas/major_tom
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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user