#include "engine/renderer.hpp" #include "engine/camera.hpp" #include "stb_image.h" #include "imgui.h" #include "imgui_impl_sdl3.h" #include "imgui_impl_sdlrenderer3.h" #include #include #include #include #include bool Renderer::init(SDL_Window* window, int width, int height) { width_ = width; height_ = height; sdl_renderer_ = SDL_CreateRenderer(window, NULL); if (!sdl_renderer_) { fprintf(stderr, "SDL_CreateRenderer failed: %s\n", SDL_GetError()); return false; } SDL_SetRenderDrawColor(sdl_renderer_, 0x1a, 0x1a, 0x2e, 0xff); const char* name = SDL_GetRendererName(sdl_renderer_); printf("Renderer: %s %dx%d\n", name ? name : "unknown", width, height); // Dear ImGui (dev UI). IMGUI_CHECKVERSION(); ImGui::CreateContext(); ImGui::StyleColorsDark(); if (ImGui_ImplSDL3_InitForSDLRenderer(window, sdl_renderer_) && ImGui_ImplSDLRenderer3_Init(sdl_renderer_)) { imgui_ready_ = true; } return true; } void Renderer::shutdown() { if (imgui_ready_) { ImGui_ImplSDLRenderer3_Shutdown(); ImGui_ImplSDL3_Shutdown(); ImGui::DestroyContext(); imgui_ready_ = false; } if (sdl_renderer_) { SDL_DestroyRenderer(sdl_renderer_); sdl_renderer_ = nullptr; } } void Renderer::begin_frame(Camera& camera) { current_cam_ = &camera; SDL_SetRenderDrawColor(sdl_renderer_, 0x1a, 0x1a, 0x2e, 0xff); SDL_RenderClear(sdl_renderer_); if (imgui_ready_) { ImGui_ImplSDLRenderer3_NewFrame(); ImGui_ImplSDL3_NewFrame(); ImGui::NewFrame(); } } void Renderer::end_frame() { if (imgui_ready_) { ImGui::Render(); ImGui_ImplSDLRenderer3_RenderDrawData(ImGui::GetDrawData(), sdl_renderer_); } SDL_RenderPresent(sdl_renderer_); current_cam_ = nullptr; } void Renderer::resize(int width, int height) { width_ = width; height_ = height; } void Renderer::draw_box(float cx, float cy, float half_w, float half_h, float angle, float r, float g, float b, float a) { if (!current_cam_) return; // Convert world coords to screen pixels const auto& cam = *current_cam_; float zoom = cam.zoom(); float vp_w = static_cast(cam.viewport_width()); float vp_h = static_cast(cam.viewport_height()); // Screen position of world origin float ox = vp_w * 0.5f - cam.position().x * zoom; float oy = vp_h * 0.5f + cam.position().y * zoom; // Box center in screen space float sx = ox + cx * zoom; float sy = oy - cy * zoom; // y flips // Box half-size in screen pixels float shw = half_w * zoom; float shh = half_h * zoom; SDL_SetRenderDrawColor(sdl_renderer_, static_cast(r * 255), static_cast(g * 255), static_cast(b * 255), static_cast(a * 255)); if (angle == 0.0f) { // Fast path: axis-aligned rect SDL_FRect rect = { sx - shw, sy - shh, shw * 2.0f, shh * 2.0f }; SDL_RenderFillRect(sdl_renderer_, &rect); } else { // Rotated: use SDL_RenderGeometry with 4 vertices float ca = std::cos(angle); float sa = std::sin(angle); SDL_Vertex verts[4]; float corners[4][2] = { {-half_w, -half_h}, { half_w, -half_h}, { half_w, half_h}, {-half_w, half_h} }; for (int i = 0; i < 4; i++) { float rx = corners[i][0] * ca - corners[i][1] * sa; float ry = corners[i][0] * sa + corners[i][1] * ca; verts[i].position.x = ox + (cx + rx) * zoom; verts[i].position.y = oy - (cy + ry) * zoom; // SDL_Vertex.color is SDL_FColor (0..1 floats), NOT 0..255. verts[i].color = {r, g, b, a}; } int indices[6] = {0, 1, 2, 0, 2, 3}; SDL_RenderGeometry(sdl_renderer_, NULL, verts, 4, indices, 6); } } SDL_Texture* Renderer::load_texture(const std::string& path) { auto it = tex_cache_.find(path); if (it != tex_cache_.end()) return it->second; // "forts:REL" resolves to the user's own Forts install (bring-your-own game // files, like a Doom source port + WAD). Nothing is copied into the repo. std::string file = path; if (path.rfind("forts:", 0) == 0) { const char* env = std::getenv("FORTS_DATA"); std::string root = env ? env : "/mnt/games/SteamLibrary/steamapps/common/Forts/data"; file = root + "/" + path.substr(6); } int w = 0, h = 0, n = 0; unsigned char* px = stbi_load(file.c_str(), &w, &h, &n, 4); SDL_Texture* tex = nullptr; if (px) { tex = SDL_CreateTexture(sdl_renderer_, SDL_PIXELFORMAT_RGBA32, SDL_TEXTUREACCESS_STATIC, w, h); if (tex) { SDL_UpdateTexture(tex, nullptr, px, w * 4); SDL_SetTextureBlendMode(tex, SDL_BLENDMODE_BLEND); SDL_SetTextureScaleMode(tex, SDL_SCALEMODE_LINEAR); } stbi_image_free(px); printf("Texture: loaded %s (%dx%d)\n", path.c_str(), w, h); } else { fprintf(stderr, "Texture: FAILED to load %s\n", path.c_str()); } tex_cache_[path] = tex; // cache even nullptr so we don't retry every frame return tex; } void Renderer::draw_sprite(float cx, float cy, float half_w, float half_h, float angle, SDL_Texture* tex, float r, float g, float b, float a) { if (!current_cam_ || !tex) return; const auto& cam = *current_cam_; float zoom = cam.zoom(); float ox = cam.viewport_width() * 0.5f - cam.position().x * zoom; float oy = cam.viewport_height() * 0.5f + cam.position().y * zoom; float ca = std::cos(angle), sa = std::sin(angle); const float corners[4][2] = { {-half_w, -half_h}, { half_w, -half_h}, { half_w, half_h}, {-half_w, half_h} }; const float uv[4][2] = { {0,1}, {1,1}, {1,0}, {0,0} }; // flip V (screen y is down) SDL_FColor col = {r, g, b, a}; SDL_Vertex v[4]; for (int i = 0; i < 4; i++) { float rx = corners[i][0]*ca - corners[i][1]*sa; float ry = corners[i][0]*sa + corners[i][1]*ca; v[i].position = { ox + (cx + rx) * zoom, oy - (cy + ry) * zoom }; v[i].color = col; v[i].tex_coord = { uv[i][0], uv[i][1] }; } int idx[6] = {0, 1, 2, 0, 2, 3}; SDL_RenderGeometry(sdl_renderer_, tex, v, 4, idx, 6); } void Renderer::draw_arc(float cx, float cy, float radius_world, float a0, float a1, float r, float g, float b, float a) { if (!current_cam_) return; const auto& cam = *current_cam_; float zoom = cam.zoom(); float ox = cam.viewport_width() * 0.5f - cam.position().x * zoom; float oy = cam.viewport_height() * 0.5f + cam.position().y * zoom; float scx = ox + cx * zoom, scy = oy - cy * zoom; // centre (screen) const float PI = 3.14159265358979323846f; int n = std::max(1, (int)(std::fabs(a1 - a0) / (2.0f * PI) * 64.0f) + 1); SDL_FColor col = {r, g, b, a}; fan_scratch_.clear(); for (int i = 0; i < n; i++) { float t0 = a0 + (a1 - a0) * (float)i / n; float t1 = a0 + (a1 - a0) * (float)(i + 1) / n; // world direction (y up) -> screen (y down) SDL_Vertex c { {scx, scy}, col, {0, 0} }; SDL_Vertex p0{ {scx + std::cos(t0) * radius_world * zoom, scy - std::sin(t0) * radius_world * zoom}, col, {0, 0} }; SDL_Vertex p1{ {scx + std::cos(t1) * radius_world * zoom, scy - std::sin(t1) * radius_world * zoom}, col, {0, 0} }; fan_scratch_.push_back(c); fan_scratch_.push_back(p0); fan_scratch_.push_back(p1); } SDL_RenderGeometry(sdl_renderer_, nullptr, fan_scratch_.data(), (int)fan_scratch_.size(), nullptr, 0); } void Renderer::draw_radial_timer(float cx, float cy, float radius_world, float frac, float r, float g, float b, float a) { if (!current_cam_) return; if (frac < 0.0f) frac = 0.0f; if (frac > 1.0f) frac = 1.0f; const auto& cam = *current_cam_; float zoom = cam.zoom(); float vp_w = static_cast(cam.viewport_width()); float vp_h = static_cast(cam.viewport_height()); float ox = vp_w * 0.5f - cam.position().x * zoom; float oy = vp_h * 0.5f + cam.position().y * zoom; float sx = ox + cx * zoom; float sy = oy - cy * zoom; float rad = radius_world * zoom; const int SEG = 32; const float PI = 3.14159265358979323846f; auto fan = [&](float start, float sweep, SDL_FColor col) { if (sweep <= 0.0f) return; int n = std::max(1, (int)(SEG * (sweep / (2.0f * PI)) + 0.5f)); fan_scratch_.clear(); // reused buffer, no per-call alloc for (int i = 0; i < n; i++) { float a0 = start + sweep * (float)i / n; float a1 = start + sweep * (float)(i + 1) / n; fan_scratch_.push_back({ {sx, sy}, col, {0, 0} }); fan_scratch_.push_back({ {sx + std::cos(a0) * rad, sy + std::sin(a0) * rad}, col, {0, 0} }); fan_scratch_.push_back({ {sx + std::cos(a1) * rad, sy + std::sin(a1) * rad}, col, {0, 0} }); } SDL_RenderGeometry(sdl_renderer_, NULL, fan_scratch_.data(), (int)fan_scratch_.size(), NULL, 0); }; // Dim background ring (full circle), then the remaining wedge on top. // Start at top (-90deg) and sweep clockwise (screen y is down). fan(-PI * 0.5f, 2.0f * PI, SDL_FColor{0.0f, 0.0f, 0.0f, 0.45f}); fan(-PI * 0.5f, 2.0f * PI * frac, SDL_FColor{r, g, b, a}); } void Renderer::draw_hud_bar(float x, float y, float fill, float r, float g, float b) { const float BAR_W = 100.0f; const float BAR_H = 14.0f; const float PAD = 2.0f; SDL_FRect bg = {x, y, BAR_W, BAR_H}; SDL_SetRenderDrawColor(sdl_renderer_, 0x20, 0x20, 0x20, 0xcc); SDL_RenderFillRect(sdl_renderer_, &bg); if (fill > 0.0f) { if (fill > 1.0f) fill = 1.0f; SDL_FRect fg = {x + PAD, y + PAD, (BAR_W - PAD * 2) * fill, BAR_H - PAD * 2}; SDL_SetRenderDrawColor(sdl_renderer_, static_cast(r * 255), static_cast(g * 255), static_cast(b * 255), 0xcc); SDL_RenderFillRect(sdl_renderer_, &fg); } }