Add moon surface intro level with asteroid hazards and unarmed mechanics

Introduce moon01.lvl as the starting level — a pure jump-and-run intro
with no gun and no enemies, just platforming over gaps and dodging falling
asteroids. The player picks up their gun upon transitioning to level01.

New features:
- Moon tileset and PARALLAX_STYLE_MOON with crater terrain backgrounds
- Asteroid entity (ENT_ASTEROID): falls from sky, damages on contact,
  explodes on ground with particles, respawns after delay
- PLAYER_UNARMED directive disables gun for the level
- Pit rescue mechanic: falling costs 1 HP and auto-dashes upward
- Gun powerup entity type for future armed-pickup levels
- Segment-based procedural level generator with themed rooms
- Extended editor with entity palette and improved tile cycling
- Web shell improvements for Emscripten builds
This commit is contained in:
Thomas
2026-03-01 09:20:49 +00:00
parent ea6e16358f
commit fac7085056
30 changed files with 2139 additions and 83 deletions

View File

@@ -11,6 +11,99 @@
#include <string.h>
#include <math.h>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
/* ═══════════════════════════════════════════════════
* Browser file I/O helpers (Emscripten only)
*
* Save: write .lvl to virtual FS, then trigger
* browser download via Blob URL.
* Load: open an <input type="file"> dialog, read
* the file contents, write to virtual FS,
* and set a flag for the editor to pick up.
* ═══════════════════════════════════════════════════ */
/* Download a file from the Emscripten virtual FS to the user's machine */
EM_JS(void, browser_download_file, (const char *vfs_path, const char *download_name), {
var path = UTF8ToString(vfs_path);
var name = UTF8ToString(download_name);
try {
var data = FS.readFile(path);
var blob = new Blob([data], { type: 'text/plain' });
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = name;
document.body.appendChild(a);
a.click();
setTimeout(function() {
document.body.removeChild(a);
URL.revokeObjectURL(url);
}, 100);
} catch (e) {
console.error('Download failed:', e);
}
});
/* Path where uploaded file will be written in virtual FS */
#define UPLOAD_VFS_PATH "/tmp/_upload.lvl"
/* Flags polled by editor_update — set by JS */
static volatile int s_upload_ready = 0; /* file upload completed */
static volatile int s_save_request = 0; /* Ctrl+S or Save button */
static volatile int s_load_request = 0; /* Ctrl+O or Load button */
/* Path to load when s_load_request is 2 (load a specific VFS file) */
static char s_load_vfs_path[256] = {0};
/* Open a browser file picker; on selection, write contents to virtual FS */
EM_JS(void, browser_open_file_picker, (), {
var input = document.createElement('input');
input.type = 'file';
input.accept = '.lvl,.txt';
input.onchange = function(e) {
var file = e.target.files[0];
if (!file) return;
var reader = new FileReader();
reader.onload = function(ev) {
var data = new Uint8Array(ev.target.result);
try { FS.unlink('/tmp/_upload.lvl'); } catch(x) {}
FS.writeFile('/tmp/_upload.lvl', data);
/* Set the C flag via direct heap access */
var ptr = _editor_upload_flag_ptr();
HEAP32[ptr >> 2] = 1;
};
reader.readAsArrayBuffer(file);
};
input.click();
});
/* Export addresses so JS can set flags and paths */
EMSCRIPTEN_KEEPALIVE
int *editor_upload_flag_ptr(void) {
return (int *)&s_upload_ready;
}
EMSCRIPTEN_KEEPALIVE
int *editor_save_flag_ptr(void) {
return (int *)&s_save_request;
}
EMSCRIPTEN_KEEPALIVE
int *editor_load_flag_ptr(void) {
return (int *)&s_load_request;
}
/* Called from JS to load an existing level file from the virtual FS */
EMSCRIPTEN_KEEPALIVE
void editor_load_vfs_file(const char *path) {
snprintf(s_load_vfs_path, sizeof(s_load_vfs_path), "%s", path);
s_load_request = 2; /* 2 = load from specific VFS path */
}
#endif /* __EMSCRIPTEN__ */
/* ═══════════════════════════════════════════════════
* Minimal 4x6 bitmap font
*
@@ -140,7 +233,7 @@ static int text_width(const char *text) {
#define ZOOM_STEP 0.25f
static const SDL_Color COL_BG = {30, 30, 46, 255};
static const SDL_Color COL_PANEL = {20, 20, 35, 240};
static const SDL_Color COL_PANEL = {20, 20, 35, 255};
static const SDL_Color COL_PANEL_LT = {40, 40, 60, 255};
static const SDL_Color COL_TEXT = {200, 200, 220, 255};
static const SDL_Color COL_TEXT_DIM = {120, 120, 140, 255};
@@ -150,9 +243,11 @@ static const SDL_Color COL_SPAWN = {60, 255, 60, 255};
static const SDL_Color COL_ENTITY = {255, 100, 100, 255};
static const SDL_Color COL_SELECT = {255, 255, 100, 255};
static const SDL_Color COL_EXIT = {50, 230, 180, 255};
/* Tool names */
static const char *s_tool_names[TOOL_COUNT] = {
"PENCIL", "ERASER", "FILL", "ENTITY", "SPAWN"
"PENCIL", "ERASER", "FILL", "ENTITY", "SPAWN", "EXIT"
};
/* Layer names */
@@ -249,6 +344,8 @@ static bool save_tilemap(const Tilemap *map, const char *path) {
fprintf(f, "PARALLAX_FAR %s\n", map->parallax_far_path);
if (map->parallax_near_path[0])
fprintf(f, "PARALLAX_NEAR %s\n", map->parallax_near_path);
if (map->player_unarmed)
fprintf(f, "PLAYER_UNARMED\n");
fprintf(f, "\n");
@@ -261,6 +358,21 @@ static bool save_tilemap(const Tilemap *map, const char *path) {
}
fprintf(f, "\n");
/* Exit zones */
for (int i = 0; i < map->exit_zone_count; i++) {
const ExitZone *ez = &map->exit_zones[i];
int tx = (int)(ez->x / TILE_SIZE);
int ty = (int)(ez->y / TILE_SIZE);
int tw = (int)(ez->w / TILE_SIZE);
int th = (int)(ez->h / TILE_SIZE);
if (ez->target[0]) {
fprintf(f, "EXIT %d %d %d %d %s\n", tx, ty, tw, th, ez->target);
} else {
fprintf(f, "EXIT %d %d %d %d\n", tx, ty, tw, th);
}
}
if (map->exit_zone_count > 0) fprintf(f, "\n");
/* Tile definitions */
for (int id = 1; id < map->tile_def_count; id++) {
const TileDef *td = &map->tile_defs[id];
@@ -449,6 +561,15 @@ bool editor_save(Editor *ed) {
}
if (save_tilemap(&ed->map, ed->file_path)) {
ed->dirty = false;
#ifdef __EMSCRIPTEN__
/* Trigger a browser download of the saved file */
{
/* Extract just the filename from the path for the download name */
const char *name = strrchr(ed->file_path, '/');
name = name ? name + 1 : ed->file_path;
browser_download_file(ed->file_path, name);
}
#endif
return true;
}
return false;
@@ -522,6 +643,42 @@ static void remove_entity_spawn(Tilemap *map, int index) {
map->entity_spawn_count--;
}
/* ── Exit zone helpers ─────────────────────────────── */
#define EXIT_DEFAULT_W 2 /* default exit zone width in tiles */
#define EXIT_DEFAULT_H 3 /* default exit zone height in tiles */
static int find_exit_at(const Tilemap *map, float wx, float wy) {
for (int i = 0; i < map->exit_zone_count; i++) {
const ExitZone *ez = &map->exit_zones[i];
if (wx >= ez->x && wx < ez->x + ez->w &&
wy >= ez->y && wy < ez->y + ez->h) {
return i;
}
}
return -1;
}
static void add_exit_zone(Tilemap *map, float wx, float wy) {
if (map->exit_zone_count >= MAX_EXIT_ZONES) return;
ExitZone *ez = &map->exit_zones[map->exit_zone_count];
/* Snap to tile grid */
ez->x = floorf(wx / TILE_SIZE) * TILE_SIZE;
ez->y = floorf(wy / TILE_SIZE) * TILE_SIZE;
ez->w = EXIT_DEFAULT_W * TILE_SIZE;
ez->h = EXIT_DEFAULT_H * TILE_SIZE;
ez->target[0] = '\0'; /* empty = victory / end of level */
map->exit_zone_count++;
}
static void remove_exit_zone(Tilemap *map, int index) {
if (index < 0 || index >= map->exit_zone_count) return;
for (int i = index; i < map->exit_zone_count - 1; i++) {
map->exit_zones[i] = map->exit_zones[i + 1];
}
map->exit_zone_count--;
}
/* ═══════════════════════════════════════════════════
* Internal state for test-play / quit requests
* ═══════════════════════════════════════════════════ */
@@ -559,6 +716,7 @@ void editor_update(Editor *ed, float dt) {
if (input_key_pressed(SDL_SCANCODE_3)) ed->tool = TOOL_FILL;
if (input_key_pressed(SDL_SCANCODE_4)) ed->tool = TOOL_ENTITY;
if (input_key_pressed(SDL_SCANCODE_5)) ed->tool = TOOL_SPAWN;
if (input_key_pressed(SDL_SCANCODE_6)) ed->tool = TOOL_EXIT;
/* Layer selection: Q/W/E */
if (input_key_pressed(SDL_SCANCODE_Q)) ed->active_layer = EDITOR_LAYER_COLLISION;
@@ -577,6 +735,55 @@ void editor_update(Editor *ed, float dt) {
editor_save(ed);
}
/* Open / Load: Ctrl+O */
if (input_key_pressed(SDL_SCANCODE_O) && input_key_held(SDL_SCANCODE_LCTRL)) {
#ifdef __EMSCRIPTEN__
browser_open_file_picker();
#else
/* Desktop: reload from current file (no native file dialog) */
if (ed->has_file) {
editor_load(ed, ed->file_path);
}
#endif
}
#ifdef __EMSCRIPTEN__
/* Poll for save request from JS (Ctrl+S or Save button) */
if (s_save_request) {
s_save_request = 0;
editor_save(ed);
}
/* Poll for load request from JS */
if (s_load_request == 1) {
/* 1 = open file picker (Ctrl+O or Load button) */
s_load_request = 0;
browser_open_file_picker();
} else if (s_load_request == 2) {
/* 2 = load a specific VFS file (level picker dropdown) */
s_load_request = 0;
if (s_load_vfs_path[0]) {
if (editor_load(ed, s_load_vfs_path)) {
printf("editor: loaded %s\n", s_load_vfs_path);
}
s_load_vfs_path[0] = '\0';
}
}
/* Poll for completed file upload from browser */
if (s_upload_ready) {
s_upload_ready = 0;
if (editor_load(ed, UPLOAD_VFS_PATH)) {
/* Clear the stored path so saves go to default,
* not the tmp upload path */
strncpy(ed->file_path, "assets/levels/edited.lvl",
sizeof(ed->file_path) - 1);
ed->has_file = true;
printf("editor: loaded from browser upload\n");
}
}
#endif
/* Test play: P */
if (input_key_pressed(SDL_SCANCODE_P)) {
s_wants_test_play = true;
@@ -642,9 +849,9 @@ void editor_update(Editor *ed, float dt) {
}
}
/* ── Zoom (scroll wheel) ── */
/* ── Zoom (scroll wheel) — only when mouse is over the canvas ── */
int scroll = input_mouse_scroll();
if (scroll != 0) {
if (scroll != 0 && in_canvas(mx, my)) {
/* Compute world pos under mouse at CURRENT zoom, before changing it */
Vec2 mouse_world = camera_screen_to_world(&ed->camera, vec2((float)mx, (float)my));
@@ -655,7 +862,7 @@ void editor_update(Editor *ed, float dt) {
/* Zoom toward mouse position: keep the world point under the
* mouse cursor in the same screen position after the zoom. */
if (ed->camera.zoom != old_zoom && in_canvas(mx, my)) {
if (ed->camera.zoom != old_zoom) {
float new_zoom = ed->camera.zoom;
ed->camera.pos.x = mouse_world.x - (float)mx / new_zoom;
ed->camera.pos.y = mouse_world.y - (float)my / new_zoom;
@@ -680,26 +887,37 @@ void editor_update(Editor *ed, float dt) {
if (max_y > 0 && ed->camera.pos.y > max_y + vp_h * 0.5f)
ed->camera.pos.y = max_y + vp_h * 0.5f;
/* ── Tile palette click (right panel) ── */
/* ── Right palette click ── */
if (mx >= SCREEN_WIDTH - EDITOR_PALETTE_W && my >= EDITOR_TOOLBAR_H &&
my < SCREEN_HEIGHT - EDITOR_STATUS_H) {
if (ed->tool != TOOL_ENTITY) {
int px = SCREEN_WIDTH - EDITOR_PALETTE_W;
int py = EDITOR_TOOLBAR_H;
int ph = SCREEN_HEIGHT - EDITOR_TOOLBAR_H - EDITOR_STATUS_H;
/* Split point: tiles take ~55%, entities take ~45% */
int tile_section_h = (ph * 55) / 100;
int ent_section_y = py + tile_section_h;
if (my < ent_section_y) {
/* Tile palette area */
if (input_mouse_pressed(MOUSE_LEFT)) {
int pal_x = mx - (SCREEN_WIDTH - EDITOR_PALETTE_W) - 2;
int pal_y = my - EDITOR_TOOLBAR_H - 2 + ed->tile_palette_scroll * TILE_SIZE;
int pal_x = mx - px - 2;
int pal_label_h = FONT_H + 6;
int pal_y = my - py - pal_label_h
+ ed->tile_palette_scroll * (TILE_SIZE + 1);
if (pal_x >= 0 && pal_y >= 0) {
int max_cols = (EDITOR_PALETTE_W - 4) / (TILE_SIZE + 1);
if (max_cols < 1) max_cols = 1;
int tile_col = pal_x / (TILE_SIZE + 1);
int tile_row = pal_y / (TILE_SIZE + 1);
if (ed->tileset_cols > 0) {
int tile_id = tile_row * ed->tileset_cols + tile_col + 1;
if (tile_id >= 1 && tile_id <= ed->tileset_total) {
ed->selected_tile = (uint16_t)tile_id;
}
int tile_id = tile_row * max_cols + tile_col + 1;
if (tile_id >= 1 && tile_id <= ed->tileset_total) {
ed->selected_tile = (uint16_t)tile_id;
/* Auto-switch to pencil tool when selecting a tile */
if (ed->tool == TOOL_ENTITY) ed->tool = TOOL_PENCIL;
}
}
}
/* Scroll palette */
if (scroll != 0) {
ed->tile_palette_scroll -= scroll;
if (ed->tile_palette_scroll < 0) ed->tile_palette_scroll = 0;
@@ -707,10 +925,14 @@ void editor_update(Editor *ed, float dt) {
} else {
/* Entity palette area */
if (input_mouse_pressed(MOUSE_LEFT)) {
int pal_y = my - EDITOR_TOOLBAR_H - 2 + ed->entity_palette_scroll * 12;
int pal_label_h = FONT_H + 6;
int pal_y = my - ent_section_y - pal_label_h
+ ed->entity_palette_scroll * 12;
int entry_idx = pal_y / 12;
if (entry_idx >= 0 && entry_idx < g_entity_registry.count) {
ed->selected_entity = entry_idx;
/* Auto-switch to entity tool when selecting an entity */
ed->tool = TOOL_ENTITY;
}
}
if (scroll != 0) {
@@ -724,13 +946,14 @@ void editor_update(Editor *ed, float dt) {
/* ── Toolbar click ── */
if (my < EDITOR_TOOLBAR_H) {
if (input_mouse_pressed(MOUSE_LEFT)) {
/* Tool buttons: each ~30px wide */
int btn = mx / 32;
/* Tool buttons: each 28px wide starting at x=2 */
int btn = (mx - 2) / 28;
if (btn >= 0 && btn < TOOL_COUNT) {
ed->tool = (EditorTool)btn;
}
/* Layer buttons at x=170+ */
int lx = mx - 170;
/* Layer buttons after separator */
int sep_x = TOOL_COUNT * 28 + 8;
int lx = mx - sep_x;
if (lx >= 0) {
int lbtn = lx / 25;
if (lbtn >= 0 && lbtn < EDITOR_LAYER_COUNT) {
@@ -835,6 +1058,25 @@ void editor_update(Editor *ed, float dt) {
}
break;
case TOOL_EXIT:
if (input_mouse_pressed(MOUSE_LEFT)) {
/* Place a new exit zone (if not clicking an existing one) */
int eidx = find_exit_at(&ed->map, world_pos.x, world_pos.y);
if (eidx < 0) {
add_exit_zone(&ed->map, world_pos.x, world_pos.y);
ed->dirty = true;
}
}
/* Right-click to delete exit zone */
if (input_mouse_pressed(MOUSE_RIGHT)) {
int eidx = find_exit_at(&ed->map, world_pos.x, world_pos.y);
if (eidx >= 0) {
remove_exit_zone(&ed->map, eidx);
ed->dirty = true;
}
}
break;
default:
break;
}
@@ -954,6 +1196,26 @@ void editor_render(Editor *ed, float interpolation) {
SDL_SetRenderDrawBlendMode(r, SDL_BLENDMODE_NONE);
}
/* ── Exit zone markers ── */
for (int i = 0; i < ed->map.exit_zone_count; i++) {
const ExitZone *ez = &ed->map.exit_zones[i];
Vec2 sp = camera_world_to_screen(cam, vec2(ez->x, ez->y));
float zw = ez->w * cam->zoom;
float zh = ez->h * cam->zoom;
SDL_SetRenderDrawColor(r, COL_EXIT.r, COL_EXIT.g, COL_EXIT.b, 80);
SDL_SetRenderDrawBlendMode(r, SDL_BLENDMODE_BLEND);
SDL_Rect er = {(int)sp.x, (int)sp.y, (int)(zw + 0.5f), (int)(zh + 0.5f)};
SDL_RenderFillRect(r, &er);
SDL_SetRenderDrawColor(r, COL_EXIT.r, COL_EXIT.g, COL_EXIT.b, 220);
SDL_RenderDrawRect(r, &er);
draw_text(r, "EXIT", (int)sp.x + 1, (int)sp.y + 1, COL_EXIT);
if (ez->target[0]) {
draw_text(r, ez->target, (int)sp.x + 1, (int)sp.y + 8, COL_EXIT);
}
SDL_SetRenderDrawBlendMode(r, SDL_BLENDMODE_NONE);
}
/* ── Cursor highlight ── */
{
int mx_c, my_c;
@@ -965,9 +1227,21 @@ void editor_render(Editor *ed, float interpolation) {
Vec2 cpos = camera_world_to_screen(cam,
vec2(tile_to_world(tx), tile_to_world(ty)));
float zs = TILE_SIZE * cam->zoom;
SDL_SetRenderDrawColor(r, COL_SELECT.r, COL_SELECT.g, COL_SELECT.b, 120);
/* Exit tool: show 2x3 preview */
int cw = 1, ch = 1;
if (ed->tool == TOOL_EXIT) {
cw = EXIT_DEFAULT_W;
ch = EXIT_DEFAULT_H;
}
SDL_SetRenderDrawColor(r,
ed->tool == TOOL_EXIT ? COL_EXIT.r : COL_SELECT.r,
ed->tool == TOOL_EXIT ? COL_EXIT.g : COL_SELECT.g,
ed->tool == TOOL_EXIT ? COL_EXIT.b : COL_SELECT.b, 120);
SDL_SetRenderDrawBlendMode(r, SDL_BLENDMODE_BLEND);
SDL_Rect cr = {(int)cpos.x, (int)cpos.y, (int)(zs + 0.5f), (int)(zs + 0.5f)};
SDL_Rect cr = {(int)cpos.x, (int)cpos.y,
(int)(zs * cw + 0.5f), (int)(zs * ch + 0.5f)};
SDL_RenderDrawRect(r, &cr);
SDL_SetRenderDrawBlendMode(r, SDL_BLENDMODE_NONE);
}
@@ -984,22 +1258,32 @@ void editor_render(Editor *ed, float interpolation) {
SDL_Rect tb = {0, 0, SCREEN_WIDTH, EDITOR_TOOLBAR_H};
SDL_RenderFillRect(r, &tb);
/* Vertically center text: (EDITOR_TOOLBAR_H - FONT_H) / 2 */
int text_y = (EDITOR_TOOLBAR_H - FONT_H) / 2;
/* Tool buttons */
for (int i = 0; i < TOOL_COUNT; i++) {
int bx = i * 32 + 2;
int bx = i * 28 + 2;
SDL_Color tc = (i == (int)ed->tool) ? COL_HIGHLIGHT : COL_TEXT_DIM;
draw_text(r, s_tool_names[i], bx, 4, tc);
draw_text(r, s_tool_names[i], bx, text_y, tc);
}
/* Separator */
int sep_x = TOOL_COUNT * 28 + 4;
SDL_SetRenderDrawColor(r, COL_PANEL_LT.r, COL_PANEL_LT.g, COL_PANEL_LT.b, 255);
SDL_RenderDrawLine(r, sep_x, 3, sep_x, EDITOR_TOOLBAR_H - 3);
/* Layer buttons */
int layer_start = sep_x + 4;
for (int i = 0; i < EDITOR_LAYER_COUNT; i++) {
int bx = 170 + i * 25;
int bx = layer_start + i * 25;
SDL_Color lc = (i == (int)ed->active_layer) ? COL_HIGHLIGHT : COL_TEXT_DIM;
draw_text(r, s_layer_names[i], bx, 4, lc);
draw_text(r, s_layer_names[i], bx, text_y, lc);
}
/* Grid & Layers indicators */
draw_text(r, ed->show_grid ? "[G]RID" : "[G]rid", 260, 4,
int grid_x = layer_start + EDITOR_LAYER_COUNT * 25 + 4;
draw_text(r, ed->show_grid ? "[G]RID" : "[G]rid", grid_x, text_y,
ed->show_grid ? COL_TEXT : COL_TEXT_DIM);
}
@@ -1013,21 +1297,26 @@ void editor_render(Editor *ed, float interpolation) {
SDL_Rect panel = {px, py, EDITOR_PALETTE_W, ph};
SDL_RenderFillRect(r, &panel);
/* Separator line */
/* Left separator line */
SDL_SetRenderDrawColor(r, COL_PANEL_LT.r, COL_PANEL_LT.g, COL_PANEL_LT.b, 255);
SDL_RenderDrawLine(r, px, py, px, py + ph);
if (ed->tool != TOOL_ENTITY) {
/* ── Tile palette ── */
draw_text(r, "TILES", px + 2, py + 2, COL_TEXT);
/* Split: tiles top ~55%, entities bottom ~45% */
int tile_section_h = (ph * 55) / 100;
int ent_section_y = py + tile_section_h;
int pal_y_start = py + 10;
/* ── Tile palette (top section) ── */
{
int label_h = FONT_H + 6; /* label area: font + padding */
draw_text(r, "TILES", px + 2, py + (label_h - FONT_H) / 2, COL_TEXT);
int pal_y_start = py + label_h;
int max_cols = (EDITOR_PALETTE_W - 4) / (TILE_SIZE + 1);
if (max_cols < 1) max_cols = 1;
if (ed->map.tileset && ed->tileset_total > 0) {
/* Set clip rect to palette area */
SDL_Rect clip = {px, pal_y_start, EDITOR_PALETTE_W, ph - 10};
SDL_Rect clip = {px, pal_y_start, EDITOR_PALETTE_W,
tile_section_h - label_h};
SDL_RenderSetClipRect(r, &clip);
for (int id = 1; id <= ed->tileset_total; id++) {
@@ -1038,11 +1327,9 @@ void editor_render(Editor *ed, float interpolation) {
int draw_y = pal_y_start + row_idx * (TILE_SIZE + 1)
- ed->tile_palette_scroll * (TILE_SIZE + 1);
/* Skip if off-screen */
if (draw_y + TILE_SIZE < pal_y_start || draw_y > py + ph)
continue;
if (draw_y + TILE_SIZE < pal_y_start ||
draw_y > ent_section_y) continue;
/* Source rect from tileset */
int src_col = (id - 1) % ed->tileset_cols;
int src_row = (id - 1) / ed->tileset_cols;
SDL_Rect src = {
@@ -1064,12 +1351,20 @@ void editor_render(Editor *ed, float interpolation) {
SDL_RenderSetClipRect(r, NULL);
}
} else {
/* ── Entity palette ── */
draw_text(r, "ENTITIES", px + 2, py + 2, COL_TEXT);
}
int pal_y_start = py + 12;
SDL_Rect clip = {px, pal_y_start, EDITOR_PALETTE_W, ph - 12};
/* ── Divider line between tiles and entities ── */
SDL_SetRenderDrawColor(r, COL_PANEL_LT.r, COL_PANEL_LT.g, COL_PANEL_LT.b, 255);
SDL_RenderDrawLine(r, px + 2, ent_section_y, px + EDITOR_PALETTE_W - 2, ent_section_y);
/* ── Entity palette (bottom section) ── */
{
int label_h = FONT_H + 6;
draw_text(r, "ENTITIES", px + 2, ent_section_y + (label_h - FONT_H) / 2, COL_TEXT);
int pal_y_start = ent_section_y + label_h;
int ent_area_h = py + ph - pal_y_start;
SDL_Rect clip = {px, pal_y_start, EDITOR_PALETTE_W, ent_area_h};
SDL_RenderSetClipRect(r, &clip);
for (int i = 0; i < g_entity_registry.count; i++) {
@@ -1113,6 +1408,6 @@ void editor_render(Editor *ed, float interpolation) {
ed->camera.zoom * 100.0f,
ed->has_file ? ed->file_path : "new level",
ed->dirty ? " *" : "");
draw_text(r, status, 2, sy + 2, COL_TEXT);
draw_text(r, status, 2, sy + (EDITOR_STATUS_H - FONT_H) / 2, COL_TEXT);
}
}