#include "engine/tilemap.h" #include "engine/camera.h" #include "engine/assets.h" #include #include #include #include /* 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 [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)); }