Files
major_tom/src/engine/tilemap.c
Thomas 372ea3d586 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.
2026-03-01 16:48:53 +00:00

296 lines
11 KiB
C

#include "engine/tilemap.h"
#include "engine/camera.h"
#include "engine/assets.h"
#include <SDL2/SDL_image.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* Read a full line from f into a dynamically growing buffer.
* *buf and *cap track the heap buffer; the caller must free *buf.
* Returns the line length, or -1 on EOF/error. */
static int read_line(FILE *f, char **buf, int *cap) {
if (!*buf) {
*cap = 16384;
*buf = malloc(*cap);
if (!*buf) return -1;
}
int len = 0;
for (;;) {
if (len + 1 >= *cap) {
int new_cap = *cap * 2;
char *tmp = realloc(*buf, new_cap);
if (!tmp) return -1;
*buf = tmp;
*cap = new_cap;
}
int ch = fgetc(f);
if (ch == EOF) return len > 0 ? len : -1;
(*buf)[len++] = (char)ch;
if (ch == '\n') break;
}
(*buf)[len] = '\0';
return len;
}
bool tilemap_load(Tilemap *map, const char *path, SDL_Renderer *renderer) {
FILE *f = fopen(path, "r");
if (!f) {
fprintf(stderr, "Failed to open level: %s\n", path);
return false;
}
memset(map, 0, sizeof(Tilemap));
char *line = NULL;
int line_cap = 0;
char tileset_path[256] = {0};
int current_layer = -1; /* 0=collision, 1=bg, 2=fg */
int row = 0;
while (read_line(f, &line, &line_cap) >= 0) {
/* Skip comments and empty lines */
if (line[0] == '#' || line[0] == '\n' || line[0] == '\r') continue;
/* Parse directives */
if (strncmp(line, "TILESET ", 8) == 0) {
sscanf(line + 8, "%255s", tileset_path);
} else if (strncmp(line, "SIZE ", 5) == 0) {
sscanf(line + 5, "%d %d", &map->width, &map->height);
if (map->width <= 0 || map->height <= 0 ||
map->width > MAX_MAP_SIZE || map->height > MAX_MAP_SIZE) {
fprintf(stderr, "Invalid map size: %dx%d\n", map->width, map->height);
free(line);
fclose(f);
return false;
}
int total = map->width * map->height;
map->collision_layer = calloc(total, sizeof(uint16_t));
map->bg_layer = calloc(total, sizeof(uint16_t));
map->fg_layer = calloc(total, sizeof(uint16_t));
if (!map->collision_layer || !map->bg_layer || !map->fg_layer) {
fprintf(stderr, "Failed to allocate tile layers (%dx%d)\n",
map->width, map->height);
free(map->collision_layer);
free(map->bg_layer);
free(map->fg_layer);
memset(map, 0, sizeof(Tilemap));
free(line);
fclose(f);
return false;
}
} else if (strncmp(line, "SPAWN ", 6) == 0) {
float sx, sy;
sscanf(line + 6, "%f %f", &sx, &sy);
map->player_spawn = vec2(sx * TILE_SIZE, sy * TILE_SIZE);
} else if (strncmp(line, "GRAVITY ", 8) == 0) {
sscanf(line + 8, "%f", &map->gravity);
} else if (strncmp(line, "TILEDEF ", 8) == 0) {
int id, tx, ty;
uint32_t flags;
sscanf(line + 8, "%d %d %d %u", &id, &tx, &ty, &flags);
if (id < MAX_TILE_DEFS) {
map->tile_defs[id].tex_x = (uint16_t)tx;
map->tile_defs[id].tex_y = (uint16_t)ty;
map->tile_defs[id].flags = flags;
if (id >= map->tile_def_count) {
map->tile_def_count = id + 1;
}
}
} else if (strncmp(line, "BG_COLOR ", 9) == 0) {
int r, g, b;
if (sscanf(line + 9, "%d %d %d", &r, &g, &b) == 3) {
map->bg_color = (SDL_Color){
(uint8_t)r, (uint8_t)g, (uint8_t)b, 255
};
map->has_bg_color = true;
}
} else if (strncmp(line, "PARALLAX_FAR ", 13) == 0) {
sscanf(line + 13, "%255s", map->parallax_far_path);
} else if (strncmp(line, "PARALLAX_NEAR ", 14) == 0) {
sscanf(line + 14, "%255s", map->parallax_near_path);
} else if (strncmp(line, "MUSIC ", 6) == 0) {
sscanf(line + 6, "%255s", map->music_path);
} else if (strncmp(line, "PARALLAX_STYLE ", 15) == 0) {
int style = 0;
if (sscanf(line + 15, "%d", &style) == 1) {
map->parallax_style = style;
}
} else if (strncmp(line, "PLAYER_UNARMED", 14) == 0) {
map->player_unarmed = true;
} else if (strncmp(line, "EXIT ", 5) == 0) {
if (map->exit_zone_count < MAX_EXIT_ZONES) {
ExitZone *ez = &map->exit_zones[map->exit_zone_count];
float tx, ty, tw, th;
char target[ASSET_PATH_MAX] = {0};
/* EXIT <tile_x> <tile_y> <tile_w> <tile_h> [target_path] */
int n = sscanf(line + 5, "%f %f %f %f %255s",
&tx, &ty, &tw, &th, target);
if (n >= 4) {
ez->x = tx * TILE_SIZE;
ez->y = ty * TILE_SIZE;
ez->w = tw * TILE_SIZE;
ez->h = th * TILE_SIZE;
if (n == 5) {
snprintf(ez->target, sizeof(ez->target), "%s", target);
} else {
ez->target[0] = '\0'; /* no target = victory */
}
map->exit_zone_count++;
}
}
} else if (strncmp(line, "ENTITY ", 7) == 0) {
if (map->entity_spawn_count < MAX_ENTITY_SPAWNS) {
EntitySpawn *es = &map->entity_spawns[map->entity_spawn_count];
float tx, ty;
if (sscanf(line + 7, "%31s %f %f", es->type_name, &tx, &ty) == 3) {
es->x = tx * TILE_SIZE;
es->y = ty * TILE_SIZE;
map->entity_spawn_count++;
}
}
} else if (strncmp(line, "LAYER ", 6) == 0) {
row = 0;
if (strstr(line, "collision")) current_layer = 0;
else if (strstr(line, "bg")) current_layer = 1;
else if (strstr(line, "fg")) current_layer = 2;
} else if (current_layer >= 0 && map->width > 0) {
/* Parse tile row */
uint16_t *layer = NULL;
switch (current_layer) {
case 0: layer = map->collision_layer; break;
case 1: layer = map->bg_layer; break;
case 2: layer = map->fg_layer; break;
}
if (layer && row < map->height) {
char *tok = strtok(line, " \t\n\r");
for (int col = 0; col < map->width && tok; col++) {
layer[row * map->width + col] = (uint16_t)atoi(tok);
tok = strtok(NULL, " \t\n\r");
}
row++;
}
}
}
free(line);
fclose(f);
/* 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;
SDL_QueryTexture(map->tileset, NULL, NULL, &tex_w, NULL);
map->tileset_cols = tex_w / TILE_SIZE;
}
}
printf("Loaded level: %s (%dx%d tiles)\n", path, map->width, map->height);
return true;
}
void tilemap_render_layer(const Tilemap *map, const uint16_t *layer,
const Camera *cam, SDL_Renderer *renderer) {
if (!map || !layer || !map->tileset) return;
/* Only render visible tiles */
int start_x = 0, start_y = 0;
int end_x = map->width, end_y = map->height;
if (cam) {
float inv_zoom = (cam->zoom > 0.0f) ? (1.0f / cam->zoom) : 1.0f;
start_x = (int)(cam->pos.x / TILE_SIZE) - 1;
start_y = (int)(cam->pos.y / TILE_SIZE) - 1;
end_x = start_x + (int)(cam->viewport.x * inv_zoom / TILE_SIZE) + 3;
end_y = start_y + (int)(cam->viewport.y * inv_zoom / TILE_SIZE) + 3;
if (start_x < 0) start_x = 0;
if (start_y < 0) start_y = 0;
if (end_x > map->width) end_x = map->width;
if (end_y > map->height) end_y = map->height;
}
for (int y = start_y; y < end_y; y++) {
for (int x = start_x; x < end_x; x++) {
uint16_t tile_id = layer[y * map->width + x];
if (tile_id == 0) continue; /* 0 = empty */
/* Look up tile definition for source rect */
SDL_Rect src;
if (tile_id < map->tile_def_count &&
(map->tile_defs[tile_id].tex_x || map->tile_defs[tile_id].tex_y)) {
src.x = map->tile_defs[tile_id].tex_x * TILE_SIZE;
src.y = map->tile_defs[tile_id].tex_y * TILE_SIZE;
} else {
/* Fallback: tile_id maps directly to tileset grid position */
int cols = map->tileset_cols > 0 ? map->tileset_cols : 16;
src.x = ((tile_id - 1) % cols) * TILE_SIZE;
src.y = ((tile_id - 1) / cols) * TILE_SIZE;
}
src.w = TILE_SIZE;
src.h = TILE_SIZE;
Vec2 world_pos = vec2(tile_to_world(x), tile_to_world(y));
Vec2 screen_pos = cam ? camera_world_to_screen(cam, world_pos) : world_pos;
float tile_draw_size = TILE_SIZE;
if (cam && cam->zoom != 1.0f && cam->zoom > 0.0f) {
tile_draw_size = TILE_SIZE * cam->zoom;
}
SDL_Rect dst = {
(int)screen_pos.x,
(int)screen_pos.y,
(int)(tile_draw_size + 0.5f),
(int)(tile_draw_size + 0.5f)
};
SDL_RenderCopy(renderer, map->tileset, &src, &dst);
}
}
}
bool tilemap_is_solid(const Tilemap *map, int tile_x, int tile_y) {
if (!map) return false;
if (tile_x < 0 || tile_x >= map->width) return true; /* out of bounds = solid */
if (tile_y < 0) return false; /* above map = open */
if (tile_y >= map->height) return true; /* below map = solid */
uint16_t tile_id = map->collision_layer[tile_y * map->width + tile_x];
if (tile_id == 0) return false;
/* Check tile def flags */
if (tile_id < map->tile_def_count) {
return (map->tile_defs[tile_id].flags & TILE_SOLID) != 0;
}
/* Default: non-zero collision tile is solid */
return true;
}
uint32_t tilemap_flags_at(const Tilemap *map, int tile_x, int tile_y) {
if (!map) return 0;
if (tile_x < 0 || tile_x >= map->width) return TILE_SOLID;
if (tile_y < 0) return 0;
if (tile_y >= map->height) return TILE_SOLID;
uint16_t tile_id = map->collision_layer[tile_y * map->width + tile_x];
if (tile_id == 0) return 0;
if (tile_id < map->tile_def_count) {
return map->tile_defs[tile_id].flags;
}
return TILE_SOLID; /* default non-zero = solid */
}
void tilemap_free(Tilemap *map) {
if (!map) return;
free(map->collision_layer);
free(map->bg_layer);
free(map->fg_layer);
/* Don't free tileset - asset manager owns it */
memset(map, 0, sizeof(Tilemap));
}