Upgrade level editor for moon campaign support
Preserve tileset path and parallax style on save instead of hardcoding tileset.png. Add tileset cycling (T key), tile flag editing (F key for solid/platform/hazard/none), and 8x8 pixel mini-icons for all entity types in the palette and canvas. Fix entity palette not appearing when editor starts without a prior level load by splitting entity_registry_init into populate and init phases. Fix bitmap font rendering dropping the top row by correcting FONT_H from 6 to 7. Widen toolbar button spacing. Fix invisible collision from placed tiles lacking TileDefs by calling ensure_tile_def on pencil and fill placement. Fix editor hotkeys not working in the web build by latching key presses from SDL_KEYDOWN events instead of comparing keyboard state snapshots.
This commit is contained in:
@@ -24,7 +24,6 @@ static int s_mouse_scroll;
|
||||
|
||||
/* ── Raw keyboard state ───────────────────────────── */
|
||||
static const Uint8 *s_key_state = NULL;
|
||||
static Uint8 s_prev_keys[SDL_NUM_SCANCODES];
|
||||
static Uint8 s_latched_keys[SDL_NUM_SCANCODES];
|
||||
|
||||
/* Default key bindings (primary + alternate) */
|
||||
@@ -53,7 +52,7 @@ void input_init(void) {
|
||||
memset(s_mouse_previous, 0, sizeof(s_mouse_previous));
|
||||
memset(s_mouse_latched_pressed, 0, sizeof(s_mouse_latched_pressed));
|
||||
memset(s_mouse_latched_released, 0, sizeof(s_mouse_latched_released));
|
||||
memset(s_prev_keys, 0, sizeof(s_prev_keys));
|
||||
|
||||
memset(s_latched_keys, 0, sizeof(s_latched_keys));
|
||||
s_mouse_x = s_mouse_y = 0;
|
||||
s_mouse_scroll = 0;
|
||||
@@ -67,11 +66,6 @@ void input_poll(void) {
|
||||
s_quit_requested = false;
|
||||
s_mouse_scroll = 0;
|
||||
|
||||
/* Save previous raw key state */
|
||||
if (s_key_state) {
|
||||
memcpy(s_prev_keys, s_key_state, SDL_NUM_SCANCODES);
|
||||
}
|
||||
|
||||
/* Process SDL events */
|
||||
SDL_Event event;
|
||||
while (SDL_PollEvent(&event)) {
|
||||
@@ -82,6 +76,16 @@ void input_poll(void) {
|
||||
case SDL_MOUSEWHEEL:
|
||||
s_mouse_scroll += event.wheel.y;
|
||||
break;
|
||||
case SDL_KEYDOWN:
|
||||
/* Latch raw key press directly from the event.
|
||||
* More reliable than state-snapshot comparison,
|
||||
* especially on Emscripten where SDL_GetKeyboardState
|
||||
* may not reflect keys pressed within the same frame. */
|
||||
if (!event.key.repeat &&
|
||||
event.key.keysym.scancode < SDL_NUM_SCANCODES) {
|
||||
s_latched_keys[event.key.keysym.scancode] = 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,15 +106,6 @@ void input_poll(void) {
|
||||
}
|
||||
}
|
||||
|
||||
/* Latch raw key edges */
|
||||
if (s_key_state) {
|
||||
for (int i = 0; i < SDL_NUM_SCANCODES; i++) {
|
||||
if (s_key_state[i] && !s_prev_keys[i]) {
|
||||
s_latched_keys[i] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Read mouse state */
|
||||
Uint32 buttons = SDL_GetMouseState(&s_mouse_x, &s_mouse_y);
|
||||
|
||||
|
||||
@@ -177,6 +177,7 @@ bool tilemap_load(Tilemap *map, const char *path, SDL_Renderer *renderer) {
|
||||
|
||||
/* Load tileset texture */
|
||||
if (tileset_path[0] && renderer) {
|
||||
snprintf(map->tileset_path, sizeof(map->tileset_path), "%s", tileset_path);
|
||||
map->tileset = assets_get_texture(tileset_path);
|
||||
if (map->tileset) {
|
||||
int tex_w;
|
||||
|
||||
@@ -47,6 +47,7 @@ typedef struct Tilemap {
|
||||
int tile_def_count;
|
||||
SDL_Texture *tileset;
|
||||
int tileset_cols; /* columns in tileset image */
|
||||
char tileset_path[ASSET_PATH_MAX]; /* tileset file path */
|
||||
Vec2 player_spawn;
|
||||
float gravity; /* level gravity (px/s^2), 0 = use default */
|
||||
char music_path[ASSET_PATH_MAX]; /* level music file path */
|
||||
|
||||
@@ -114,7 +114,7 @@ void editor_load_vfs_file(const char *path) {
|
||||
* ═══════════════════════════════════════════════════ */
|
||||
|
||||
#define FONT_W 4
|
||||
#define FONT_H 6
|
||||
#define FONT_H 7
|
||||
|
||||
/* 4-bit rows packed: row0 in bits 20-23, row1 in 16-19, etc.
|
||||
* Bit order: MSB = leftmost pixel */
|
||||
@@ -223,6 +223,62 @@ static int text_width(const char *text) {
|
||||
}
|
||||
#endif
|
||||
|
||||
/* ═══════════════════════════════════════════════════
|
||||
* 8x8 pixel mini-icons for the entity palette
|
||||
*
|
||||
* Each icon is 8 pixels wide, 8 pixels tall.
|
||||
* Stored as 8 rows of 8 bits packed in a uint64_t.
|
||||
* Row 0 is the top row, stored in bits 56-63.
|
||||
* Bit 7 of each byte = leftmost pixel.
|
||||
* ═══════════════════════════════════════════════════ */
|
||||
|
||||
#define ICON_SIZE 8
|
||||
|
||||
static const uint64_t s_icon_bitmaps[ICON_COUNT] = {
|
||||
/* ICON_GRUNT: spiky creature */
|
||||
0x5A3C7EDB7E3C5A00ULL,
|
||||
/* ICON_FLYER: bat with wings */
|
||||
0x0018187E7E241800ULL,
|
||||
/* ICON_TURRET: mounted cannon */
|
||||
0x0010107C7C381000ULL,
|
||||
/* ICON_PLATFORM: horizontal bar */
|
||||
0x000000007E7E0000ULL,
|
||||
/* ICON_PLATFORM_V: vertical bar */
|
||||
0x0018181818180000ULL,
|
||||
/* ICON_FLAME: fire */
|
||||
0x0010385454381000ULL,
|
||||
/* ICON_FORCEFIELD: electric bars */
|
||||
0x2424FF2424FF2424ULL,
|
||||
/* ICON_HEART: classic heart shape */
|
||||
0x006C7E7E3E1C0800ULL,
|
||||
/* ICON_BOLT: lightning bolt */
|
||||
0x0C1C18387060C000ULL,
|
||||
/* ICON_DRONE: quad-rotor */
|
||||
0x00427E3C3C7E4200ULL,
|
||||
/* ICON_GUN: pistol shape */
|
||||
0x00007C7C10100000ULL,
|
||||
/* ICON_ASTEROID: jagged rock */
|
||||
0x001C3E7F7F3E1C00ULL,
|
||||
/* ICON_SPACECRAFT: ship */
|
||||
0x0018183C7E7E2400ULL,
|
||||
};
|
||||
|
||||
static void draw_icon(SDL_Renderer *r, EditorIcon icon,
|
||||
int x, int y, SDL_Color col) {
|
||||
if (icon < 0 || icon >= ICON_COUNT) return;
|
||||
uint64_t bits = s_icon_bitmaps[icon];
|
||||
SDL_SetRenderDrawColor(r, col.r, col.g, col.b, col.a);
|
||||
|
||||
for (int row = 0; row < ICON_SIZE; row++) {
|
||||
int byte = (int)((bits >> (56 - row * 8)) & 0xFF);
|
||||
for (int col_bit = 0; col_bit < ICON_SIZE; col_bit++) {
|
||||
if (byte & (1 << (7 - col_bit))) {
|
||||
SDL_RenderDrawPoint(r, x + col_bit, y + row);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════
|
||||
* Constants
|
||||
* ═══════════════════════════════════════════════════ */
|
||||
@@ -232,6 +288,16 @@ static int text_width(const char *text) {
|
||||
#define ZOOM_MAX 2.0f
|
||||
#define ZOOM_STEP 0.25f
|
||||
|
||||
/* ═══════════════════════════════════════════════════
|
||||
* Available tilesets (cycle with T key)
|
||||
* ═══════════════════════════════════════════════════ */
|
||||
|
||||
static const char *s_tilesets[] = {
|
||||
"assets/tiles/tileset.png",
|
||||
"assets/tiles/moon_tileset.png",
|
||||
};
|
||||
#define TILESET_COUNT ((int)(sizeof(s_tilesets) / sizeof(s_tilesets[0])))
|
||||
|
||||
static const SDL_Color COL_BG = {30, 30, 46, 255};
|
||||
static const SDL_Color COL_PANEL = {20, 20, 35, 255};
|
||||
static const SDL_Color COL_PANEL_LT = {40, 40, 60, 255};
|
||||
@@ -372,7 +438,9 @@ static bool save_tilemap(const Tilemap *map, const char *path) {
|
||||
}
|
||||
|
||||
fprintf(f, "# Level created with in-game editor\n\n");
|
||||
fprintf(f, "TILESET assets/tiles/tileset.png\n");
|
||||
/* Write the tileset path stored in the map, falling back to default */
|
||||
fprintf(f, "TILESET %s\n",
|
||||
map->tileset_path[0] ? map->tileset_path : "assets/tiles/tileset.png");
|
||||
fprintf(f, "SIZE %d %d\n", map->width, map->height);
|
||||
|
||||
/* Player spawn in tile coords */
|
||||
@@ -390,6 +458,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->parallax_style > 0)
|
||||
fprintf(f, "PARALLAX_STYLE %d\n", map->parallax_style);
|
||||
if (map->player_unarmed)
|
||||
fprintf(f, "PLAYER_UNARMED\n");
|
||||
|
||||
@@ -513,6 +583,82 @@ static void discover_tileset(Editor *ed) {
|
||||
ed->tileset_total = ed->tileset_cols * ed->tileset_rows;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════
|
||||
* Tileset switching
|
||||
* ═══════════════════════════════════════════════════ */
|
||||
|
||||
/* Find current tileset index in the table, or -1 */
|
||||
static int find_tileset_index(const char *path) {
|
||||
for (int i = 0; i < TILESET_COUNT; i++) {
|
||||
if (strcmp(path, s_tilesets[i]) == 0) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Switch the editor to a different tileset by path */
|
||||
static void editor_switch_tileset(Editor *ed, const char *path) {
|
||||
SDL_Texture *tex = assets_get_texture(path);
|
||||
if (!tex) {
|
||||
fprintf(stderr, "editor: cannot load tileset %s\n", path);
|
||||
return;
|
||||
}
|
||||
snprintf(ed->map.tileset_path, sizeof(ed->map.tileset_path), "%s", path);
|
||||
ed->map.tileset = tex;
|
||||
int tex_w;
|
||||
SDL_QueryTexture(tex, NULL, NULL, &tex_w, NULL);
|
||||
ed->map.tileset_cols = tex_w / TILE_SIZE;
|
||||
discover_tileset(ed);
|
||||
ed->tile_palette_scroll = 0;
|
||||
ed->dirty = true;
|
||||
}
|
||||
|
||||
/* Cycle to the next tileset in the table.
|
||||
* If the current tileset is not in the list, cycle to the first entry. */
|
||||
static void editor_cycle_tileset(Editor *ed) {
|
||||
int idx = find_tileset_index(ed->map.tileset_path);
|
||||
int next = (idx < 0) ? 0 : (idx + 1) % TILESET_COUNT;
|
||||
editor_switch_tileset(ed, s_tilesets[next]);
|
||||
printf("editor: tileset -> %s\n", s_tilesets[next]);
|
||||
}
|
||||
|
||||
/* ── Tile flag helpers ───────────────────────────── */
|
||||
|
||||
/* Ensure a tile ID has a TileDef entry. Creates one if missing. */
|
||||
static void ensure_tile_def(Tilemap *map, uint16_t tile_id) {
|
||||
if (tile_id == 0 || tile_id >= MAX_TILE_DEFS) return;
|
||||
if (tile_id < map->tile_def_count) return;
|
||||
/* Fill gaps with default entries (tex pos from grid, no flags) */
|
||||
int cols = map->tileset_cols > 0 ? map->tileset_cols : 16;
|
||||
for (int id = map->tile_def_count; id <= tile_id; id++) {
|
||||
map->tile_defs[id].tex_x = (uint16_t)((id - 1) % cols);
|
||||
map->tile_defs[id].tex_y = (uint16_t)((id - 1) / cols);
|
||||
map->tile_defs[id].flags = 0;
|
||||
}
|
||||
map->tile_def_count = tile_id + 1;
|
||||
}
|
||||
|
||||
/* Cycle the flag on the currently selected tile: none -> solid -> platform -> hazard -> none */
|
||||
static void editor_cycle_tile_flag(Editor *ed) {
|
||||
uint16_t id = ed->selected_tile;
|
||||
if (id == 0 || id >= MAX_TILE_DEFS) return;
|
||||
ensure_tile_def(&ed->map, id);
|
||||
uint32_t cur = ed->map.tile_defs[id].flags;
|
||||
/* Cycle: 0 -> SOLID -> PLATFORM -> HAZARD -> 0 */
|
||||
if (cur & TILE_HAZARD) ed->map.tile_defs[id].flags = 0;
|
||||
else if (cur & TILE_PLATFORM) ed->map.tile_defs[id].flags = TILE_HAZARD;
|
||||
else if (cur & TILE_SOLID) ed->map.tile_defs[id].flags = TILE_PLATFORM;
|
||||
else ed->map.tile_defs[id].flags = TILE_SOLID;
|
||||
ed->dirty = true;
|
||||
}
|
||||
|
||||
/* Get display name for the flags of the selected tile */
|
||||
static const char *tile_flag_name(uint32_t flags) {
|
||||
if (flags & TILE_HAZARD) return "HAZARD";
|
||||
if (flags & TILE_PLATFORM) return "PLATFORM";
|
||||
if (flags & TILE_SOLID) return "SOLID";
|
||||
return "NONE";
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════
|
||||
* Lifecycle
|
||||
* ═══════════════════════════════════════════════════ */
|
||||
@@ -528,6 +674,13 @@ void editor_init(Editor *ed) {
|
||||
ed->dragging_entity = -1;
|
||||
ed->active = true;
|
||||
camera_init(&ed->camera, SCREEN_WIDTH, SCREEN_HEIGHT);
|
||||
|
||||
/* Ensure entity registry is populated for the palette.
|
||||
* If we came from gameplay it is already initialized;
|
||||
* if the editor was launched directly it will be empty. */
|
||||
if (g_entity_registry.count == 0) {
|
||||
entity_registry_populate();
|
||||
}
|
||||
}
|
||||
|
||||
void editor_new_level(Editor *ed, int width, int height) {
|
||||
@@ -555,7 +708,9 @@ void editor_new_level(Editor *ed, int width, int height) {
|
||||
ed->map.gravity = DEFAULT_GRAVITY;
|
||||
|
||||
/* Load tileset */
|
||||
ed->map.tileset = assets_get_texture("assets/tiles/tileset.png");
|
||||
snprintf(ed->map.tileset_path, sizeof(ed->map.tileset_path),
|
||||
"%s", "assets/tiles/tileset.png");
|
||||
ed->map.tileset = assets_get_texture(ed->map.tileset_path);
|
||||
if (ed->map.tileset) {
|
||||
int tex_w;
|
||||
SDL_QueryTexture(ed->map.tileset, NULL, NULL, &tex_w, NULL);
|
||||
@@ -776,6 +931,14 @@ void editor_update(Editor *ed, float dt) {
|
||||
/* Layer visibility toggle: V */
|
||||
if (input_key_pressed(SDL_SCANCODE_V)) ed->show_all_layers = !ed->show_all_layers;
|
||||
|
||||
/* Tileset cycle: T */
|
||||
if (input_key_pressed(SDL_SCANCODE_T) && !input_key_held(SDL_SCANCODE_LCTRL))
|
||||
editor_cycle_tileset(ed);
|
||||
|
||||
/* Tile flag cycle: F (solid/platform/hazard/none) */
|
||||
if (input_key_pressed(SDL_SCANCODE_F) && !input_key_held(SDL_SCANCODE_LCTRL))
|
||||
editor_cycle_tile_flag(ed);
|
||||
|
||||
/* Save: Ctrl+S */
|
||||
if (input_key_pressed(SDL_SCANCODE_S) && input_key_held(SDL_SCANCODE_LCTRL)) {
|
||||
editor_save(ed);
|
||||
@@ -992,13 +1155,13 @@ void editor_update(Editor *ed, float dt) {
|
||||
/* ── Toolbar click ── */
|
||||
if (my < EDITOR_TOOLBAR_H) {
|
||||
if (input_mouse_pressed(MOUSE_LEFT)) {
|
||||
/* Tool buttons: each 28px wide starting at x=2 */
|
||||
int btn = (mx - 2) / 28;
|
||||
/* Tool buttons: each 35px wide starting at x=2 */
|
||||
int btn = (mx - 2) / 35;
|
||||
if (btn >= 0 && btn < TOOL_COUNT) {
|
||||
ed->tool = (EditorTool)btn;
|
||||
}
|
||||
/* Layer buttons after separator */
|
||||
int sep_x = TOOL_COUNT * 28 + 8;
|
||||
int sep_x = TOOL_COUNT * 35 + 8;
|
||||
int lx = mx - sep_x;
|
||||
if (lx >= 0) {
|
||||
int lbtn = lx / 25;
|
||||
@@ -1022,6 +1185,9 @@ void editor_update(Editor *ed, float dt) {
|
||||
case TOOL_PENCIL:
|
||||
if (input_mouse_held(MOUSE_LEFT)) {
|
||||
uint16_t *layer = get_layer(&ed->map, ed->active_layer);
|
||||
/* Ensure tile has an explicit TileDef so it does not fall
|
||||
* through to the "unknown tile = solid" default. */
|
||||
ensure_tile_def(&ed->map, ed->selected_tile);
|
||||
set_tile(&ed->map, layer, tile_x, tile_y, ed->selected_tile);
|
||||
ed->dirty = true;
|
||||
}
|
||||
@@ -1044,6 +1210,7 @@ void editor_update(Editor *ed, float dt) {
|
||||
case TOOL_FILL:
|
||||
if (input_mouse_pressed(MOUSE_LEFT)) {
|
||||
uint16_t *layer = get_layer(&ed->map, ed->active_layer);
|
||||
ensure_tile_def(&ed->map, ed->selected_tile);
|
||||
flood_fill(&ed->map, layer, tile_x, tile_y, ed->selected_tile);
|
||||
ed->dirty = true;
|
||||
}
|
||||
@@ -1192,6 +1359,36 @@ void editor_render(Editor *ed, float interpolation) {
|
||||
SDL_SetRenderDrawBlendMode(r, SDL_BLENDMODE_NONE);
|
||||
}
|
||||
|
||||
/* ── Hazard tile overlay (collision layer) ── */
|
||||
if (ed->map.collision_layer) {
|
||||
float inv_zoom = (cam->zoom > 0.0f) ? (1.0f / cam->zoom) : 1.0f;
|
||||
int hs_tx = (int)floorf(cam->pos.x / TILE_SIZE) - 1;
|
||||
int hs_ty = (int)floorf(cam->pos.y / TILE_SIZE) - 1;
|
||||
int he_tx = hs_tx + (int)(cam->viewport.x * inv_zoom / TILE_SIZE) + 3;
|
||||
int he_ty = hs_ty + (int)(cam->viewport.y * inv_zoom / TILE_SIZE) + 3;
|
||||
if (hs_tx < 0) hs_tx = 0;
|
||||
if (hs_ty < 0) hs_ty = 0;
|
||||
if (he_tx > ed->map.width) he_tx = ed->map.width;
|
||||
if (he_ty > ed->map.height) he_ty = ed->map.height;
|
||||
|
||||
SDL_SetRenderDrawBlendMode(r, SDL_BLENDMODE_BLEND);
|
||||
for (int y = hs_ty; y < he_ty; y++) {
|
||||
for (int x = hs_tx; x < he_tx; x++) {
|
||||
uint16_t tid = ed->map.collision_layer[y * ed->map.width + x];
|
||||
if (tid == 0 || tid >= ed->map.tile_def_count) continue;
|
||||
if (!(ed->map.tile_defs[tid].flags & TILE_HAZARD)) continue;
|
||||
Vec2 sp = camera_world_to_screen(cam,
|
||||
vec2(tile_to_world(x), tile_to_world(y)));
|
||||
float zs = TILE_SIZE * cam->zoom;
|
||||
SDL_SetRenderDrawColor(r, 255, 60, 30, 60);
|
||||
SDL_Rect hr = {(int)sp.x, (int)sp.y,
|
||||
(int)(zs + 0.5f), (int)(zs + 0.5f)};
|
||||
SDL_RenderFillRect(r, &hr);
|
||||
}
|
||||
}
|
||||
SDL_SetRenderDrawBlendMode(r, SDL_BLENDMODE_NONE);
|
||||
}
|
||||
|
||||
/* ── Level boundary ── */
|
||||
{
|
||||
SDL_SetRenderDrawColor(r, 200, 200, 255, 120);
|
||||
@@ -1224,8 +1421,11 @@ void editor_render(Editor *ed, float interpolation) {
|
||||
SDL_RenderDrawRect(r, &er);
|
||||
SDL_SetRenderDrawBlendMode(r, SDL_BLENDMODE_NONE);
|
||||
|
||||
/* Draw first letter of entity name */
|
||||
if (reg && reg->display[0] && zw >= 6) {
|
||||
/* Draw icon or first letter of entity name */
|
||||
if (reg && zw >= ICON_SIZE + 2 && reg->icon >= 0 && reg->icon < ICON_COUNT) {
|
||||
draw_icon(r, (EditorIcon)reg->icon,
|
||||
(int)sp.x + 1, (int)sp.y + 1, COL_TEXT);
|
||||
} else if (reg && reg->display[0] && zw >= 6) {
|
||||
draw_char(r, reg->display[0], (int)sp.x + 1, (int)sp.y + 1, COL_TEXT);
|
||||
}
|
||||
}
|
||||
@@ -1309,13 +1509,13 @@ void editor_render(Editor *ed, float interpolation) {
|
||||
|
||||
/* Tool buttons */
|
||||
for (int i = 0; i < TOOL_COUNT; i++) {
|
||||
int bx = i * 28 + 2;
|
||||
int bx = i * 35 + 2;
|
||||
SDL_Color tc = (i == (int)ed->tool) ? COL_HIGHLIGHT : COL_TEXT_DIM;
|
||||
draw_text(r, s_tool_names[i], bx, text_y, tc);
|
||||
}
|
||||
|
||||
/* Separator */
|
||||
int sep_x = TOOL_COUNT * 28 + 4;
|
||||
int sep_x = TOOL_COUNT * 35 + 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);
|
||||
|
||||
@@ -1331,6 +1531,10 @@ void editor_render(Editor *ed, float interpolation) {
|
||||
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);
|
||||
|
||||
/* Tileset switch hint */
|
||||
int ts_x = grid_x + 7 * (FONT_W + 1) + 4;
|
||||
draw_text(r, "[T]SET", ts_x, text_y, COL_TEXT_DIM);
|
||||
}
|
||||
|
||||
/* ── Right palette panel ── */
|
||||
@@ -1354,7 +1558,13 @@ void editor_render(Editor *ed, float interpolation) {
|
||||
/* ── 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);
|
||||
/* Show tileset filename (strip directory) */
|
||||
{
|
||||
const char *ts_name = strrchr(ed->map.tileset_path, '/');
|
||||
ts_name = ts_name ? ts_name + 1 : ed->map.tileset_path;
|
||||
draw_text(r, ts_name[0] ? ts_name : "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);
|
||||
@@ -1385,6 +1595,24 @@ void editor_render(Editor *ed, float interpolation) {
|
||||
SDL_Rect dst = {draw_x, draw_y, TILE_SIZE, TILE_SIZE};
|
||||
SDL_RenderCopy(r, ed->map.tileset, &src, &dst);
|
||||
|
||||
/* Flag indicator: colored dot for solid/platform/hazard */
|
||||
if (id < ed->map.tile_def_count) {
|
||||
uint32_t fl = ed->map.tile_defs[id].flags;
|
||||
if (fl & TILE_HAZARD) {
|
||||
SDL_SetRenderDrawColor(r, 255, 80, 40, 255);
|
||||
SDL_Rect dot = {draw_x + TILE_SIZE - 3, draw_y, 3, 3};
|
||||
SDL_RenderFillRect(r, &dot);
|
||||
} else if (fl & TILE_PLATFORM) {
|
||||
SDL_SetRenderDrawColor(r, 80, 200, 255, 255);
|
||||
SDL_Rect dot = {draw_x + TILE_SIZE - 3, draw_y, 3, 3};
|
||||
SDL_RenderFillRect(r, &dot);
|
||||
} else if (fl & TILE_SOLID) {
|
||||
SDL_SetRenderDrawColor(r, 200, 200, 200, 180);
|
||||
SDL_Rect dot = {draw_x + TILE_SIZE - 3, draw_y, 3, 3};
|
||||
SDL_RenderFillRect(r, &dot);
|
||||
}
|
||||
}
|
||||
|
||||
/* Highlight selected */
|
||||
if (id == (int)ed->selected_tile) {
|
||||
SDL_SetRenderDrawColor(r, COL_HIGHLIGHT.r, COL_HIGHLIGHT.g,
|
||||
@@ -1397,6 +1625,21 @@ void editor_render(Editor *ed, float interpolation) {
|
||||
|
||||
SDL_RenderSetClipRect(r, NULL);
|
||||
}
|
||||
|
||||
/* Show selected tile's flag below palette */
|
||||
if (ed->selected_tile > 0 && ed->selected_tile < MAX_TILE_DEFS) {
|
||||
uint32_t flags = 0;
|
||||
if (ed->selected_tile < ed->map.tile_def_count)
|
||||
flags = ed->map.tile_defs[ed->selected_tile].flags;
|
||||
const char *fname = tile_flag_name(flags);
|
||||
SDL_Color fc = (flags & TILE_HAZARD) ? (SDL_Color){255, 80, 40, 255} :
|
||||
(flags & TILE_PLATFORM) ? (SDL_Color){80, 200, 255, 255} :
|
||||
(flags & TILE_SOLID) ? COL_TEXT : COL_TEXT_DIM;
|
||||
draw_text(r, fname, px + 2, ent_section_y - FONT_H - 2, fc);
|
||||
/* Show [F] hint */
|
||||
int fw = (int)strlen(fname) * (FONT_W + 1);
|
||||
draw_text(r, "[F]", px + 2 + fw + 2, ent_section_y - FONT_H - 2, COL_TEXT_DIM);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Divider line between tiles and entities ── */
|
||||
@@ -1419,11 +1662,16 @@ void editor_render(Editor *ed, float interpolation) {
|
||||
|
||||
if (ey + 10 < pal_y_start || ey > py + ph) continue;
|
||||
|
||||
/* Color swatch */
|
||||
SDL_SetRenderDrawColor(r, ent->color.r, ent->color.g,
|
||||
ent->color.b, 255);
|
||||
SDL_Rect swatch = {px + 2, ey + 1, 8, 8};
|
||||
SDL_RenderFillRect(r, &swatch);
|
||||
/* Mini-icon (or color swatch fallback) */
|
||||
if (ent->icon >= 0 && ent->icon < ICON_COUNT) {
|
||||
draw_icon(r, (EditorIcon)ent->icon,
|
||||
px + 2, ey + 1, ent->color);
|
||||
} else {
|
||||
SDL_SetRenderDrawColor(r, ent->color.r, ent->color.g,
|
||||
ent->color.b, 255);
|
||||
SDL_Rect swatch = {px + 2, ey + 1, 8, 8};
|
||||
SDL_RenderFillRect(r, &swatch);
|
||||
}
|
||||
|
||||
/* Name */
|
||||
SDL_Color nc = (i == ed->selected_entity) ? COL_HIGHLIGHT : COL_TEXT;
|
||||
|
||||
@@ -39,7 +39,8 @@ static Entity *spawn_powerup_gun(EntityManager *em, Vec2 pos) {
|
||||
/* ── Registry population ─────────────────────────── */
|
||||
|
||||
static void reg_add(const char *name, const char *display,
|
||||
EntitySpawnFn fn, SDL_Color color, int w, int h) {
|
||||
EntitySpawnFn fn, SDL_Color color, int w, int h,
|
||||
int icon) {
|
||||
EntityRegistry *r = &g_entity_registry;
|
||||
if (r->count >= MAX_REGISTRY_ENTRIES) return;
|
||||
r->entries[r->count++] = (EntityRegEntry){
|
||||
@@ -49,12 +50,48 @@ static void reg_add(const char *name, const char *display,
|
||||
.color = color,
|
||||
.width = w,
|
||||
.height = h,
|
||||
.icon = icon,
|
||||
};
|
||||
}
|
||||
|
||||
void entity_registry_init(EntityManager *em) {
|
||||
/* ── Registry table population (no EntityManager needed) ── */
|
||||
|
||||
void entity_registry_populate(void) {
|
||||
g_entity_registry.count = 0;
|
||||
|
||||
/* ════════════════════════════════════════════
|
||||
* REGISTRY TABLE
|
||||
*
|
||||
* To add a new entity type to the game:
|
||||
* 1. Create its .h/.c files
|
||||
* 2. Register its behaviors in entity_registry_init()
|
||||
* 3. Add one reg_add() line below
|
||||
*
|
||||
* The editor will pick it up automatically.
|
||||
* ════════════════════════════════════════════ */
|
||||
|
||||
/* .lvl name display name spawn fn color (editor) w h icon */
|
||||
reg_add("grunt", "Grunt", grunt_spawn, (SDL_Color){200, 60, 60, 255}, GRUNT_WIDTH, GRUNT_HEIGHT, ICON_GRUNT);
|
||||
reg_add("flyer", "Flyer", flyer_spawn, (SDL_Color){140, 80, 200, 255}, FLYER_WIDTH, FLYER_HEIGHT, ICON_FLYER);
|
||||
reg_add("turret", "Turret", turret_spawn, (SDL_Color){160, 160, 160, 255}, TURRET_WIDTH, TURRET_HEIGHT, ICON_TURRET);
|
||||
reg_add("platform", "Platform (H)", mplat_spawn, (SDL_Color){80, 180, 80, 255}, MPLAT_WIDTH, MPLAT_HEIGHT, ICON_PLATFORM);
|
||||
reg_add("platform_v", "Platform (V)", spawn_platform_v, (SDL_Color){80, 160, 100, 255}, MPLAT_WIDTH, MPLAT_HEIGHT, ICON_PLATFORM_V);
|
||||
reg_add("flame_vent", "Flame Vent", flame_vent_spawn, (SDL_Color){255, 120, 40, 255}, FLAME_WIDTH, FLAME_HEIGHT, ICON_FLAME);
|
||||
reg_add("force_field", "Force Field", force_field_spawn, (SDL_Color){60, 140, 255, 255}, FFIELD_WIDTH, FFIELD_HEIGHT, ICON_FORCEFIELD);
|
||||
reg_add("powerup_hp", "Health Pickup", spawn_powerup_health, (SDL_Color){255, 80, 80, 255}, 12, 12, ICON_HEART);
|
||||
reg_add("powerup_jet", "Jetpack Refill", spawn_powerup_jetpack,(SDL_Color){255, 200, 50, 255}, 12, 12, ICON_BOLT);
|
||||
reg_add("powerup_drone", "Drone Pickup", spawn_powerup_drone, (SDL_Color){80, 200, 255, 255}, 12, 12, ICON_DRONE);
|
||||
reg_add("powerup_gun", "Gun Pickup", spawn_powerup_gun, (SDL_Color){200, 200, 220, 255}, 12, 12, ICON_GUN);
|
||||
reg_add("asteroid", "Asteroid", asteroid_spawn, (SDL_Color){140, 110, 80, 255}, ASTEROID_WIDTH, ASTEROID_HEIGHT, ICON_ASTEROID);
|
||||
reg_add("spacecraft", "Spacecraft", spacecraft_spawn, (SDL_Color){187, 187, 187, 255}, SPACECRAFT_WIDTH, SPACECRAFT_HEIGHT, ICON_SPACECRAFT);
|
||||
|
||||
printf("Entity registry: %d types registered\n", g_entity_registry.count);
|
||||
}
|
||||
|
||||
void entity_registry_init(EntityManager *em) {
|
||||
/* Populate the registry table first */
|
||||
entity_registry_populate();
|
||||
|
||||
/* Register all entity behaviors with the entity manager */
|
||||
player_register(em);
|
||||
player_set_entity_manager(em);
|
||||
@@ -69,34 +106,6 @@ void entity_registry_init(EntityManager *em) {
|
||||
drone_register(em);
|
||||
asteroid_register(em);
|
||||
spacecraft_register(em);
|
||||
|
||||
/* ════════════════════════════════════════════
|
||||
* REGISTRY TABLE
|
||||
*
|
||||
* To add a new entity type to the game:
|
||||
* 1. Create its .h/.c files
|
||||
* 2. Register its behaviors above
|
||||
* 3. Add one reg_add() line below
|
||||
*
|
||||
* The editor will pick it up automatically.
|
||||
* ════════════════════════════════════════════ */
|
||||
|
||||
/* .lvl name display name spawn fn color (editor) w h */
|
||||
reg_add("grunt", "Grunt", grunt_spawn, (SDL_Color){200, 60, 60, 255}, GRUNT_WIDTH, GRUNT_HEIGHT);
|
||||
reg_add("flyer", "Flyer", flyer_spawn, (SDL_Color){140, 80, 200, 255}, FLYER_WIDTH, FLYER_HEIGHT);
|
||||
reg_add("turret", "Turret", turret_spawn, (SDL_Color){160, 160, 160, 255}, TURRET_WIDTH, TURRET_HEIGHT);
|
||||
reg_add("platform", "Platform (H)", mplat_spawn, (SDL_Color){80, 180, 80, 255}, MPLAT_WIDTH, MPLAT_HEIGHT);
|
||||
reg_add("platform_v", "Platform (V)", spawn_platform_v, (SDL_Color){80, 160, 100, 255}, MPLAT_WIDTH, MPLAT_HEIGHT);
|
||||
reg_add("flame_vent", "Flame Vent", flame_vent_spawn, (SDL_Color){255, 120, 40, 255}, FLAME_WIDTH, FLAME_HEIGHT);
|
||||
reg_add("force_field", "Force Field", force_field_spawn, (SDL_Color){60, 140, 255, 255}, FFIELD_WIDTH, FFIELD_HEIGHT);
|
||||
reg_add("powerup_hp", "Health Pickup", spawn_powerup_health, (SDL_Color){255, 80, 80, 255}, 12, 12);
|
||||
reg_add("powerup_jet", "Jetpack Refill", spawn_powerup_jetpack,(SDL_Color){255, 200, 50, 255}, 12, 12);
|
||||
reg_add("powerup_drone", "Drone Pickup", spawn_powerup_drone, (SDL_Color){80, 200, 255, 255}, 12, 12);
|
||||
reg_add("powerup_gun", "Gun Pickup", spawn_powerup_gun, (SDL_Color){200, 200, 220, 255}, 12, 12);
|
||||
reg_add("asteroid", "Asteroid", asteroid_spawn, (SDL_Color){140, 110, 80, 255}, ASTEROID_WIDTH, ASTEROID_HEIGHT);
|
||||
reg_add("spacecraft", "Spacecraft", spacecraft_spawn, (SDL_Color){187, 187, 187, 255}, SPACECRAFT_WIDTH, SPACECRAFT_HEIGHT);
|
||||
|
||||
printf("Entity registry: %d types registered\n", g_entity_registry.count);
|
||||
}
|
||||
|
||||
const EntityRegEntry *entity_registry_find(const char *name) {
|
||||
|
||||
@@ -22,6 +22,28 @@
|
||||
/* Spawn function: creates an entity at position, returns it */
|
||||
typedef Entity *(*EntitySpawnFn)(EntityManager *em, Vec2 pos);
|
||||
|
||||
/* ── Editor mini-icon indices ────────────────────── */
|
||||
/* Shared between the editor (bitmap definitions) and
|
||||
* the registry (icon field in each entry). Keep in
|
||||
* sync with s_icon_bitmaps[] in editor.c. */
|
||||
typedef enum EditorIcon {
|
||||
ICON_GRUNT = 0, /* spiky enemy */
|
||||
ICON_FLYER = 1, /* bat / wing */
|
||||
ICON_TURRET = 2, /* cannon */
|
||||
ICON_PLATFORM = 3, /* horizontal bar */
|
||||
ICON_PLATFORM_V = 4, /* vertical bar */
|
||||
ICON_FLAME = 5, /* fire / flame vent */
|
||||
ICON_FORCEFIELD = 6, /* electric field */
|
||||
ICON_HEART = 7, /* health pickup */
|
||||
ICON_BOLT = 8, /* jetpack / lightning */
|
||||
ICON_DRONE = 9, /* drone companion */
|
||||
ICON_GUN = 10, /* weapon pickup */
|
||||
ICON_ASTEROID = 11, /* rock */
|
||||
ICON_SPACECRAFT = 12, /* ship */
|
||||
ICON_COUNT,
|
||||
ICON_NONE = -1 /* no icon (fallback) */
|
||||
} EditorIcon;
|
||||
|
||||
typedef struct EntityRegEntry {
|
||||
const char *name; /* .lvl file name, e.g. "grunt" */
|
||||
const char *display; /* human-readable, e.g. "Grunt" */
|
||||
@@ -29,6 +51,7 @@ typedef struct EntityRegEntry {
|
||||
SDL_Color color; /* editor palette color for preview */
|
||||
int width; /* hitbox width (for editor preview) */
|
||||
int height; /* hitbox height (for editor preview) */
|
||||
int icon; /* editor mini-icon index (-1 = none) */
|
||||
} EntityRegEntry;
|
||||
|
||||
typedef struct EntityRegistry {
|
||||
@@ -43,6 +66,11 @@ extern EntityRegistry g_entity_registry;
|
||||
* Also registers entity behaviors with the given EntityManager. */
|
||||
void entity_registry_init(EntityManager *em);
|
||||
|
||||
/* Populate only the registry table (names, colors, icons) without
|
||||
* registering entity behaviors. Safe to call without an EntityManager.
|
||||
* Used by the editor when it starts without a prior level load. */
|
||||
void entity_registry_populate(void);
|
||||
|
||||
/* Look up a registry entry by name (returns NULL if not found) */
|
||||
const EntityRegEntry *entity_registry_find(const char *name);
|
||||
|
||||
|
||||
@@ -150,7 +150,9 @@ bool level_load_generated(Level *level, Tilemap *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");
|
||||
snprintf(level->map.tileset_path, sizeof(level->map.tileset_path),
|
||||
"%s", "assets/tiles/tileset.png");
|
||||
level->map.tileset = assets_get_texture(level->map.tileset_path);
|
||||
if (level->map.tileset) {
|
||||
int tex_w;
|
||||
SDL_QueryTexture(level->map.tileset, NULL, NULL, &tex_w, NULL);
|
||||
|
||||
Reference in New Issue
Block a user