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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user