#include "kknd.h" static void render_message_log(Game *g); /* ========================================================================= * render.c - All 2D drawing: terrain, units, buildings, HUD and minimap. * Recreates the chunky overhead KKND battlefield presentation. * ========================================================================= */ int render_init(Game *g) { if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) != 0) { fprintf(stderr, "SDL_Init: %s\n", SDL_GetError()); return 0; } if (TTF_Init() != 0) { fprintf(stderr, "TTF_Init: %s\n", TTF_GetError()); return 0; } g->win = SDL_CreateWindow(KKND_NAME " - Krush, Kill 'n' Destroy", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, VIEW_W, VIEW_H, SDL_WINDOW_SHOWN); if (!g->win) return 0; g->ren = SDL_CreateRenderer(g->win, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC); if (!g->ren) return 0; SDL_SetRenderDrawBlendMode(g->ren, SDL_BLENDMODE_BLEND); sprites_init(&g->spr, g->ren); return 1; } void render_text(Sprites *s, SDL_Renderer *r, TTF_Font *f, const char *txt, int x, int y, SDL_Color c) { if (!f || !txt || !*txt) return; SDL_Surface *surf = TTF_RenderText_Blended(f, txt, c); if (!surf) return; SDL_Texture *tex = SDL_CreateTextureFromSurface(r, surf); if (tex) { SDL_Rect dr = { x, y, surf->w, surf->h }; SDL_RenderCopy(r, tex, NULL, &dr); SDL_DestroyTexture(tex); } SDL_FreeSurface(surf); } static void draw_tex_world(Game *g, SDL_Texture *tex, float wx, float wy, int w, int h, float angle, Uint8 alpha) { SDL_Rect r; r.w = w; r.h = h; r.x = (int)(wx - w/2 - g->cam.x); r.y = (int)(wy - h/2 - g->cam.y); SDL_SetTextureAlphaMod(tex, alpha); SDL_RenderCopyEx(g->ren, tex, NULL, &r, angle * 180.0f / 3.14159f, NULL, SDL_FLIP_NONE); SDL_SetTextureAlphaMod(tex, 255); } void render_world(Game *g) { int tx0 = (int)(g->cam.x / TILE) - 1; int ty0 = (int)(g->cam.y / TILE) - 1; int tx1 = (int)((g->cam.x + VIEW_W) / TILE) + 1; int ty1 = (int)((g->cam.y + VIEW_H) / TILE) + 1; for (int ty = ty0; ty <= ty1; ty++) { for (int tx = tx0; tx <= tx1; tx++) { if (!map_in_bounds(&g->map, tx, ty)) continue; uint8_t fv = g->map.fog[ty*g->map.w + tx]; if (fv == 0) continue; /* unexplored: leave black */ Tile *t = map_tile(&g->map, tx, ty); SDL_Rect r = { (int)(tx*TILE - g->cam.x), (int)(ty*TILE - g->cam.y), TILE, TILE }; SDL_RenderCopy(g->ren, g->spr.terrain[t->terrain], NULL, &r); if (fv == 1) { SDL_SetRenderDrawColor(g->ren, 0, 0, 0, 120); SDL_RenderFillRect(g->ren, &r); } } } /* subtle grid for the tactical look (only on explored tiles) */ SDL_SetRenderDrawColor(g->ren, 0, 0, 0, 22); for (int tx = tx0; tx <= tx1; tx++) { int x = (int)(tx*TILE - g->cam.x); SDL_RenderDrawLine(g->ren, x, (int)(ty0*TILE-g->cam.y), x, (int)(ty1*TILE-g->cam.y)); } for (int ty = ty0; ty <= ty1; ty++) { int y = (int)(ty*TILE - g->cam.y); SDL_RenderDrawLine(g->ren, (int)(tx0*TILE-g->cam.x), y, (int)(tx1*TILE-g->cam.x), y); } } void render_buildings(Game *g) { for (int i = 0; i < g->bld_count; i++) { Building *b = &g->buildings[i]; if (b->dead) continue; if (b->faction != g->human) { int tx = b->tx + b->w/2, ty = b->ty + b->h/2; if (!map_in_bounds(&g->map, tx, ty) || g->map.fog[ty*g->map.w + tx] != 2) continue; } int W = b->w * TILE, H = b->h * TILE; int sx = (int)(b->tx*TILE - g->cam.x); int sy = (int)(b->ty*TILE - g->cam.y); SDL_Rect r = { sx, sy, W, H }; SDL_Texture *tex = g->spr.bld[b->type][b->faction]; if (b->build < 1.0f) { /* construction: ghost body + KKND2-style yard scaffold */ SDL_SetTextureAlphaMod(tex, 160); SDL_RenderCopy(g->ren, tex, NULL, &r); SDL_SetTextureAlphaMod(tex, 255); int mid = sy + (int)(H * b->build); /* corner posts + rails */ SDL_SetRenderDrawColor(g->ren, 175, 145, 85, 255); SDL_RenderDrawLine(g->ren, sx, sy, sx, sy + H); SDL_RenderDrawLine(g->ren, sx + W - 1, sy, sx + W - 1, sy + H); SDL_RenderDrawLine(g->ren, sx, sy + H - 1, sx + W, sy + H - 1); SDL_SetRenderDrawColor(g->ren, 210, 175, 100, 255); SDL_RenderDrawLine(g->ren, sx, sy, sx + W, sy); SDL_RenderDrawLine(g->ren, sx, mid, sx + W, mid); /* X-braces + optional mid post */ SDL_SetRenderDrawColor(g->ren, 155, 125, 65, 230); SDL_RenderDrawLine(g->ren, sx + 2, sy + 2, sx + W - 2, sy + H - 2); SDL_RenderDrawLine(g->ren, sx + W - 2, sy + 2, sx + 2, sy + H - 2); if (W >= 48) SDL_RenderDrawLine(g->ren, sx + W / 2, sy, sx + W / 2, sy + H); /* progress bar above */ SDL_Rect bar = { sx, sy - 8, W, 5 }; SDL_SetRenderDrawColor(g->ren, 200, 200, 200, 255); SDL_RenderFillRect(g->ren, &bar); SDL_Rect fill = { sx, sy - 8, (int)(W * b->build), 5 }; SDL_SetRenderDrawColor(g->ren, 120, 220, 120, 255); SDL_RenderFillRect(g->ren, &fill); } else { SDL_RenderCopy(g->ren, tex, NULL, &r); } /* battle wear: scorch + crack lines when damaged */ if (b->build >= 1.0f && b->max_hp > 0 && b->hp < b->max_hp * 0.95f) { float dmg = 1.0f - (b->hp / b->max_hp); int pw = W / 4; if (pw < 4) pw = 4; int ph = H / 5; if (ph < 3) ph = 3; SDL_SetRenderDrawColor(g->ren, 42, 32, 22, (Uint8)(90 + (int)(dmg * 110))); SDL_RenderFillRect(g->ren, &(SDL_Rect){ sx + W / 4, sy + H / 3, pw, ph }); if (dmg > 0.3f) SDL_RenderFillRect(g->ren, &(SDL_Rect){ sx + W / 2, sy + H / 2, pw, ph }); SDL_SetRenderDrawColor(g->ren, 25, 22, 18, 255); SDL_RenderDrawLine(g->ren, sx + 4, sy + H / 4, sx + W / 2, sy + (H * 2) / 3); SDL_RenderDrawLine(g->ren, sx + W / 2, sy + (H * 2) / 3, sx + W - 4, sy + H / 3); if (dmg > 0.5f) { SDL_RenderDrawLine(g->ren, sx + W / 3, sy + 2, sx + W / 3 + 4, sy + H - 2); SDL_SetRenderDrawColor(g->ren, 90, 75, 55, 255); SDL_RenderFillRect(g->ren, &(SDL_Rect){ sx + 2, sy + H - 4, 6, 4 }); SDL_RenderFillRect(g->ren, &(SDL_Rect){ sx + W - 10, sy + H - 5, 8, 5 }); } } /* outlined HP strip (green→red) when damaged or selected */ if (b->max_hp > 0 && (b->hp < b->max_hp * 0.95f || b->selected)) { int by = sy - 6; float f = b->hp / b->max_hp; if (f < 0) f = 0; if (f > 1) f = 1; SDL_Rect outline = { sx - 1, by - 1, W + 2, 5 }; SDL_SetRenderDrawColor(g->ren, 20, 18, 14, 255); SDL_RenderFillRect(g->ren, &outline); SDL_Rect bg = { sx, by, W, 3 }; SDL_SetRenderDrawColor(g->ren, 55, 48, 36, 255); SDL_RenderFillRect(g->ren, &bg); Uint8 hr = (Uint8)(255 * (1.0f - f)), hg = (Uint8)(230 * f); SDL_Rect fill = { sx, by, (int)(W * f + 0.5f), 3 }; SDL_SetRenderDrawColor(g->ren, hr, hg, 40, 255); SDL_RenderFillRect(g->ren, &fill); } if (b->selected) { SDL_SetRenderDrawColor(g->ren, 255, 255, 255, 200); SDL_RenderDrawRect(g->ren, &r); } /* production progress */ if (b->producing != 0) { const UnitDef *ud = unit_def((UnitType)b->producing, b->faction); float f = b->prod_timer / (float)ud->build_time; SDL_Rect bar = { sx, sy + H + 2, W, 4 }; SDL_SetRenderDrawColor(g->ren, 60, 60, 60, 255); SDL_RenderFillRect(g->ren, &bar); SDL_Rect fill = { sx, sy + H + 2, (int)(W * f), 4 }; SDL_SetRenderDrawColor(g->ren, 255, 220, 120, 255); SDL_RenderFillRect(g->ren, &fill); } } } void render_wrecks(Game *g) { for (int i = 0; i < g->wreck_count; i++) { Wreck *w = &g->wrecks[i]; int tx = (int)(w->x / TILE), ty = (int)(w->y / TILE); if (!map_in_bounds(&g->map, tx, ty)) continue; if (g->map.fog[ty*g->map.w + tx] == 0) continue; SDL_Color c = faction_color(w->faction); int sx = (int)(w->x - g->cam.x), sy = (int)(w->y - g->cam.y); SDL_SetRenderDrawColor(g->ren, c.r/2, c.g/2, c.b/2, 200); SDL_RenderFillRect(g->ren, &(SDL_Rect){ sx-12, sy-9, 24, 18 }); SDL_SetRenderDrawColor(g->ren, 30, 28, 26, 200); SDL_RenderFillRect(g->ren, &(SDL_Rect){ sx-8, sy-5, 16, 10 }); } } /* Per-type live silhouette size (parity with snapshot SHOT_UNIT_SCALE kinds). */ static int unit_screen_sz(UnitType t) { switch (t) { case UT_INFANTRY: return 36; case UT_MUTANT: return 38; case UT_JEEP: case UT_BUGGY: return 40; case UT_FLAME: case UT_MISSILE: case UT_MEDIC: return 42; case UT_ARTILLERY: case UT_HARVESTER: return 46; case UT_TANK: return 48; case UT_BEAST: return 50; case UT_RAVAGER: return 52; default: return 41; } } /* Unrotated ground shadow disc (must sit under rotated unit tex). */ static void draw_unit_shadow(SDL_Renderer *ren, int cx, int cy, int rad) { SDL_SetRenderDrawColor(ren, 55, 48, 28, 200); for (int j = -rad; j <= rad; j++) { int half = (int)sqrtf((float)(rad * rad - j * j)); SDL_RenderDrawLine(ren, cx - half + 2, cy + j + 3, cx + half + 2, cy + j + 3); } } /* Filled annulus selection ring (parity with snapshot ring t≈2). */ static void draw_sel_ring(SDL_Renderer *ren, int cx, int cy, int rad, int t) { int inner = rad - t; if (inner < 0) inner = 0; int r2 = rad * rad, i2 = inner * inner; SDL_SetRenderDrawColor(ren, 255, 245, 60, 220); for (int y = -rad; y <= rad; y++) { for (int x = -rad; x <= rad; x++) { int d = x * x + y * y; if (d <= r2 && d >= i2) SDL_RenderDrawPoint(ren, cx + x, cy + y); } } } static int live_unit_wheeled(UnitType t) { return t == UT_BUGGY || t == UT_JEEP || t == UT_HARVESTER || t == UT_BEAST || t == UT_MISSILE || t == UT_MEDIC; } static int live_unit_tracked(UnitType t) { return t == UT_TANK || t == UT_RAVAGER; } static int live_unit_rolling(UnitType t) { return live_unit_wheeled(t) || live_unit_tracked(t); } static int live_unit_footed(UnitType t) { return t == UT_INFANTRY || t == UT_MUTANT; } /* Spoke (wheeled) / hash (tracked) on tyre discs; phase = Unit.anim rad. * Local 30×30 chassis coords match snapshot LX/LY, scaled to live sz. */ static void live_tyre_mark(SDL_Renderer *ren, int cx, int cy, int rad, float phase, int wheeled) { int n = wheeled ? 4 : 3; float step = (float)(2.0 * M_PI / (double)n); float r0 = wheeled ? rad * 0.30f : rad * 0.55f; float r1 = rad * 0.90f; SDL_SetRenderDrawColor(ren, 205, 200, 180, 240); for (int i = 0; i < n; i++) { float a = phase + i * step; int x1 = cx + (int)(cosf(a) * r0); int y1 = cy + (int)(sinf(a) * r0); int x2 = cx + (int)(cosf(a) * r1); int y2 = cy + (int)(sinf(a) * r1); SDL_RenderDrawLine(ren, x1, y1, x2, y2); } } static void live_draw_tyre_marks(SDL_Renderer *ren, Unit *u, int cx, int cy, int sz) { if (!live_unit_rolling(u->type)) return; float c = cosf(u->heading), s = sinf(u->heading); float sc = (float)sz / 30.0f; float ph = u->anim; #define LTX(lx, ly) (cx + (int)((((lx)-15)*sc)*c - (((ly)-15)*sc)*s)) #define LTY(lx, ly) (cy + (int)((((lx)-15)*sc)*s + (((ly)-15)*sc)*c)) #define LRAD(n) ((int)((n) * sc + 0.5f)) switch (u->type) { case UT_BUGGY: live_tyre_mark(ren, LTX(8,7), LTY(8,7), LRAD(4), ph, 1); live_tyre_mark(ren, LTX(22,7), LTY(22,7), LRAD(4), ph, 1); live_tyre_mark(ren, LTX(8,23), LTY(8,23), LRAD(4), ph, 1); live_tyre_mark(ren, LTX(22,23),LTY(22,23),LRAD(4), ph, 1); break; case UT_TANK: live_tyre_mark(ren, LTX(8,23), LTY(8,23), LRAD(4), ph, 0); live_tyre_mark(ren, LTX(22,23),LTY(22,23),LRAD(4), ph, 0); break; case UT_HARVESTER: live_tyre_mark(ren, LTX(8,23), LTY(8,23), LRAD(4), ph, 1); live_tyre_mark(ren, LTX(22,23),LTY(22,23),LRAD(4), ph, 1); break; case UT_JEEP: live_tyre_mark(ren, LTX(9,22), LTY(9,22), LRAD(4), ph, 1); live_tyre_mark(ren, LTX(21,22),LTY(21,22),LRAD(4), ph, 1); break; case UT_BEAST: live_tyre_mark(ren, LTX(9,21), LTY(9,21), LRAD(3), ph, 1); live_tyre_mark(ren, LTX(14,22),LTY(14,22),LRAD(3), ph, 1); live_tyre_mark(ren, LTX(19,21),LTY(19,21),LRAD(3), ph, 1); live_tyre_mark(ren, LTX(23,22),LTY(23,22),LRAD(3), ph, 1); break; case UT_RAVAGER: live_tyre_mark(ren, LTX(7,23), LTY(7,23), LRAD(4), ph, 0); live_tyre_mark(ren, LTX(23,23),LTY(23,23),LRAD(4), ph, 0); break; case UT_MISSILE: live_tyre_mark(ren, LTX(9,23), LTY(9,23), LRAD(4), ph, 1); live_tyre_mark(ren, LTX(21,23),LTY(21,23),LRAD(4), ph, 1); break; case UT_MEDIC: live_tyre_mark(ren, LTX(10,24),LTY(10,24),LRAD(4), ph, 1); live_tyre_mark(ren, LTX(20,24),LTY(20,24),LRAD(4), ph, 1); break; default: break; } #undef LTX #undef LTY #undef LRAD } void render_units(Game *g) { for (int i = 0; i < g->unit_count; i++) { Unit *u = &g->units[i]; if (u->dead) continue; if (u->faction != g->human) { int tx = (int)(u->x / TILE), ty = (int)(u->y / TILE); if (!map_in_bounds(&g->map, tx, ty) || g->map.fog[ty*g->map.w + tx] != 2) continue; } int sz = unit_screen_sz(u->type); int cx = (int)(u->x - g->cam.x), cy = (int)(u->y - g->cam.y); /* Ground shadow before rotated body so units lift off sand. */ draw_unit_shadow(g->ren, cx, cy, sz / 4 + 4); /* Foot infantry/mutant: vertical body bob; shadow stays grounded. */ float bob_y = 0.0f; if (live_unit_footed(u->type)) { bob_y = sinf(u->anim) * 2.5f; cy += (int)(bob_y + (bob_y >= 0.0f ? 0.5f : -0.5f)); } SDL_Texture *tex = g->spr.unit[u->type][u->faction]; draw_tex_world(g, tex, u->x, u->y + bob_y, sz, sz, u->heading, 255); /* rotating spokes / tread hashes (parity with snapshot tyre_mark) */ live_draw_tyre_marks(g->ren, u, cx, cy, sz); /* cream heading tick (parity with snapshot LX/LY overlay) */ { float tick = (float)(sz / 2 + 6); int hx = cx + (int)(cosf(u->heading) * tick); int hy = cy + (int)(sinf(u->heading) * tick); SDL_SetRenderDrawColor(g->ren, 250, 245, 210, 255); SDL_RenderDrawLine(g->ren, cx, cy, hx, hy); SDL_SetRenderDrawColor(g->ren, 255, 250, 220, 220); SDL_RenderDrawLine(g->ren, cx + 1, cy, hx + 1, hy); } /* thick selection annulus (parity with snapshot ring t=2) */ if (u->selected) { int rad = (u->type == UT_TANK || u->type == UT_BEAST || u->type == UT_RAVAGER) ? sz / 2 + 4 : sz / 2 + 2; draw_sel_ring(g->ren, cx, cy, rad, 2); } /* always-on HP bar when max_hp known (parity with snapshot) */ if (u->max_hp > 0) { int by = cy - sz / 2 - 8; int bw = sz - 6; if (bw < 24) bw = 24; SDL_Rect outline = { cx - bw / 2 - 1, by - 1, bw + 2, 6 }; SDL_SetRenderDrawColor(g->ren, 20, 18, 14, 255); SDL_RenderFillRect(g->ren, &outline); SDL_Rect bg = { cx - bw / 2, by, bw, 4 }; SDL_SetRenderDrawColor(g->ren, 55, 48, 36, 255); SDL_RenderFillRect(g->ren, &bg); float f = u->hp / u->max_hp; if (f < 0) f = 0; if (f > 1) f = 1; Uint8 hr = (Uint8)(255 * (1.0f - f)), hg = (Uint8)(230 * f); SDL_Rect fg = { cx - bw / 2, by, (int)(bw * f + 0.5f), 4 }; SDL_SetRenderDrawColor(g->ren, hr, hg, 40, 255); SDL_RenderFillRect(g->ren, &fg); /* cargo indicator for harvesters */ if (u->type == UT_HARVESTER && u->cargo > 0) { SDL_Rect cg = { cx - bw / 2, by - 4, (int)(bw * u->cargo / 1000), 2 }; SDL_SetRenderDrawColor(g->ren, 230, 190, 70, 255); SDL_RenderFillRect(g->ren, &cg); } } } } void render_bullets(Game *g) { /* Kind-aware tracers / shells with streak tails (parity with snapshot.c). */ for (int i = 0; i < g->bullet_count; i++) { Bullet *b = &g->bullets[i]; int sx = (int)(b->x - g->cam.x), sy = (int)(b->y - g->cam.y); float dx = b->tx - b->x, dy = b->ty - b->y; float d = sqrtf(dx * dx + dy * dy); if (d > 1.0f) { float ux = dx / d, uy = dy / d; int len = (b->kind == 2) ? 24 : (b->kind == 3) ? 18 : (b->kind == 1) ? 14 : 10; for (int t = 1; t <= len; t++) { int tx = sx - (int)(ux * (float)t * 0.75f); int ty = sy - (int)(uy * (float)t * 0.75f); Uint8 fade = (Uint8)(255 - t * 9); if (b->kind == 2) { /* laser: cyan→magenta streak */ SDL_SetRenderDrawColor(g->ren, fade, 80, 255, 220); SDL_RenderFillRect(g->ren, &(SDL_Rect){ tx - 1, ty - 1, (t < 3) ? 3 : 2, (t < 3) ? 3 : 2 }); SDL_SetRenderDrawColor(g->ren, 220, 180, 255, 255); SDL_RenderDrawPoint(g->ren, tx, ty); } else if (b->kind == 3) { SDL_SetRenderDrawColor(g->ren, 255, (Uint8)(120 - t * 4), 30, 230); SDL_RenderFillRect(g->ren, &(SDL_Rect){ tx - 1, ty - 1, (t < 4) ? 3 : 2, (t < 4) ? 3 : 2 }); } else { SDL_SetRenderDrawColor(g->ren, 255, (Uint8)(200 - t * 8), 50, 220); SDL_RenderFillRect(g->ren, &(SDL_Rect){ tx, ty, 2, 2 }); } } } int rad = (b->kind == 1 || b->kind == 3) ? 6 : 4; SDL_SetRenderDrawColor(g->ren, 255, 160, 40, 255); SDL_RenderFillRect(g->ren, &(SDL_Rect){ sx - rad - 1, sy - rad - 1, (rad + 1) * 2, (rad + 1) * 2 }); SDL_SetRenderDrawColor(g->ren, 255, 240, 140, 255); SDL_RenderFillRect(g->ren, &(SDL_Rect){ sx - rad, sy - rad, rad * 2, rad * 2 }); if (b->kind == 2) { SDL_SetRenderDrawColor(g->ren, 120, 200, 255, 255); SDL_RenderFillRect(g->ren, &(SDL_Rect){ sx - rad - 1, sy - rad - 1, (rad + 1) * 2, (rad + 1) * 2 }); SDL_SetRenderDrawColor(g->ren, 240, 220, 255, 255); SDL_RenderFillRect(g->ren, &(SDL_Rect){ sx - (rad - 1), sy - (rad - 1), (rad - 1) * 2, (rad - 1) * 2 }); } else if (b->kind == 3) { SDL_SetRenderDrawColor(g->ren, 255, 70, 30, 240); SDL_RenderFillRect(g->ren, &(SDL_Rect){ sx - 5, sy - 1, 6, 6 }); SDL_SetRenderDrawColor(g->ren, 255, 200, 80, 240); SDL_RenderFillRect(g->ren, &(SDL_Rect){ sx, sy - 4, 4, 4 }); } /* Keep sprite head as bright core on top of procedural tracer. */ SDL_Texture *tex = g->spr.bullet[b->kind < 4 ? b->kind : 0]; SDL_Rect r = { sx - 5, sy - 5, 10, 10 }; SDL_RenderCopy(g->ren, tex, NULL, &r); } } /* Soft disc for smoke/fire particles (parity with snapshot disc FX). */ static void draw_particle_disc(SDL_Renderer *ren, int cx, int cy, int rad, Uint8 r, Uint8 g, Uint8 b, Uint8 a) { SDL_SetRenderDrawColor(ren, r, g, b, a); for (int j = -rad; j <= rad; j++) { int half = (int)sqrtf((float)(rad * rad - j * j)); SDL_RenderDrawLine(ren, cx - half, cy + j, cx + half, cy + j); } } void render_particles(Game *g) { /* Kind-aware combat FX: discs for smoke/fire, rects for sparks/debris. */ for (int i = 0; i < g->particle_count; i++) { Particle *p = &g->particles[i]; int sx = (int)(p->x - g->cam.x), sy = (int)(p->y - g->cam.y); float life_a = p->life / (p->max_life > 0 ? p->max_life : 1.0f); if (life_a < 0.12f) continue; Uint8 a = (Uint8)(255 * life_a); Uint8 rr = (Uint8)(p->r * life_a), gg = (Uint8)(p->g * life_a), bb = (Uint8)(p->b * life_a); int sz = (p->kind == 1) ? 5 : (p->kind == 3) ? 3 : (p->kind == 0) ? 4 : 3; if (p->kind == 0 || p->kind == 1) { draw_particle_disc(g->ren, sx, sy, sz + 1, rr, gg, bb, a); } else { SDL_SetRenderDrawColor(g->ren, rr, gg, bb, a); SDL_RenderFillRect(g->ren, &(SDL_Rect){ sx - sz/2, sy - sz/2, sz + 1, sz + 1 }); } } } void render_selection(Game *g) { if (g->dragging) { int x = g->drag_x0 < g->drag_x1 ? g->drag_x0 : g->drag_x1; int y = g->drag_y0 < g->drag_y1 ? g->drag_y0 : g->drag_y1; int w = abs(g->drag_x1 - g->drag_x0); int h = abs(g->drag_y1 - g->drag_y0); SDL_SetRenderDrawColor(g->ren, 120, 255, 120, 120); SDL_RenderDrawRect(g->ren, &(SDL_Rect){x, y, w, h}); SDL_SetRenderDrawColor(g->ren, 120, 255, 120, 30); SDL_RenderFillRect(g->ren, &(SDL_Rect){x, y, w, h}); } /* placement ghost */ if (g->placing) { int mx, my; SDL_GetMouseState(&mx, &my); float wx, wy; input_screen_to_world(g, mx, my, &wx, &wy); int tx, ty; map_world_to_tile(wx, wy, &tx, &ty); int ok = bld_can_place(g, g->place_type, tx, ty); const BldDef *d = bld_def(g->place_type); int sx = (int)(tx*TILE - g->cam.x), sy = (int)(ty*TILE - g->cam.y); SDL_Rect r = { sx, sy, d->w*TILE, d->h*TILE }; SDL_SetRenderDrawColor(g->ren, ok ? 120 : 220, ok ? 220 : 80, 80, 140); SDL_RenderFillRect(g->ren, &r); SDL_SetRenderDrawColor(g->ren, ok ? 120 : 220, ok ? 220 : 80, 80, 255); SDL_RenderDrawRect(g->ren, &r); } } void hud_layout(Game *g, HudButton *out, int *n) { int count = 0; int bx = 12, by = VIEW_H - 64, bw = 92, bh = 52, gap = 6; /* always-available structures (player faction) */ BuildingType structs[] = { BT_WARFACTORY, BT_REFINERY, BT_POWER, BT_RESEARCH, BT_REPAIR, BT_TURRET, BT_WALL }; for (int i = 0; i < 7; i++) { const BldDef *d = bld_def(structs[i]); out[count].x = bx; out[count].y = by; out[count].w = bw; out[count].h = bh; out[count].kind = 0; out[count].id = structs[i]; out[count].label = d->name; out[count].cost = d->cost; out[count].enabled = (g->scrap[g->human] >= d->cost); bx += bw + gap; count++; if (bx + bw > VIEW_W - 12) { bx = 12; by -= (bh + 6); } } /* if a production building is selected, show its units */ for (int i = 0; i < g->bld_count; i++) { Building *b = &g->buildings[i]; if (b->dead || !b->selected || b->faction != g->human) continue; const BldDef *d = bld_def(b->type); for (int k = 0; k < d->produces_count; k++) { const UnitDef *ud = unit_def((UnitType)d->produces[k], g->human); out[count].x = bx; out[count].y = by; out[count].w = bw; out[count].h = bh; out[count].kind = 1; out[count].id = d->produces[k]; out[count].label = ud->name; out[count].cost = ud->cost; out[count].enabled = (g->scrap[g->human] >= ud->cost); bx += bw + gap; count++; if (bx + bw > VIEW_W - 12) { bx = 12; by -= (bh + 6); } } } /* if a research lab is selected, show upgrade buttons */ { int has_lab = 0; for (int i = 0; i < g->bld_count; i++) { Building *b = &g->buildings[i]; if (b->dead || !b->selected || b->faction != g->human) continue; const BldDef *d = bld_def(b->type); if (d->is_research) { has_lab = 1; break; } } if (has_lab) { for (int u = 0; u < UP_COUNT; u++) { int lvl = g->upg[g->human].lvl[u]; int maxed = (lvl >= UP_MAX_LVL); int cost = maxed ? 0 : research_cost(u, lvl); int busy = (g->research_cur[g->human] == u); out[count].x = bx; out[count].y = by; out[count].w = bw; out[count].h = bh; out[count].kind = 2; out[count].id = u; out[count].label = research_name(u); out[count].cost = cost; out[count].enabled = !maxed && !busy && g->scrap[g->human] >= cost; bx += bw + gap; count++; if (bx + bw > VIEW_W - 12) { bx = 12; by -= (bh + 6); } } } } /* command buttons for the current unit selection (appended to build row) */ { int sel_units = 0, sel_harv = 0; for (int i = 0; i < g->sel_count; i++) { Unit *u = unit_by_id(g, g->selection[i]); if (u && u->faction == g->human) { sel_units++; if (u->type == UT_HARVESTER) sel_harv++; } } if (sel_units > 0) { #define ADDCMD(cid, lab) do { \ out[count].x = bx; out[count].y = by; out[count].w = bw; out[count].h = bh; \ out[count].kind = 3; out[count].id = (cid); out[count].label = (lab); \ out[count].enabled = 1; out[count].cost = 0; count++; \ bx += bw + gap; \ if (bx + bw > VIEW_W - 12) { bx = 12; by -= (bh + 6); } \ } while (0) ADDCMD(0, "Stop"); ADDCMD(1, "Attack"); ADDCMD(4, "Hold"); if (sel_harv > 0) { ADDCMD(2, "Gather"); ADDCMD(3, "Return"); } #undef ADDCMD /* special-ability button (siege / heal) when applicable */ int ab = AB_NONE; for (int i = 0; i < g->sel_count; i++) { Unit *uu = unit_by_id(g, g->selection[i]); if (uu && uu->faction == g->human && uu->ability != AB_NONE) { ab = uu->ability; break; } } if (ab != AB_NONE) { const char *lab = (ab == AB_SIEGE) ? "Siege" : "Heal"; out[count].x = bx; out[count].y = by; out[count].w = bw; out[count].h = bh; out[count].kind = 4; out[count].id = ab; out[count].label = lab; out[count].enabled = 1; out[count].cost = 0; count++; bx += bw + gap; if (bx + bw > VIEW_W - 12) { bx = 12; by -= (bh + 6); } } } } *n = count; } static char g_btn_label[256][64]; void render_build_menu(Game *g) { HudButton btns[64]; int n = 0; hud_layout(g, btns, &n); /* panel background (covers build bar + command row) */ SDL_SetRenderDrawColor(g->ren, 20, 20, 24, 230); SDL_RenderFillRect(g->ren, &(SDL_Rect){0, VIEW_H - 138, VIEW_W, 138}); SDL_SetRenderDrawColor(g->ren, 90, 160, 70, 255); SDL_RenderDrawLine(g->ren, 0, VIEW_H - 138, VIEW_W, VIEW_H - 138); SDL_SetRenderDrawColor(g->ren, 60, 90, 50, 255); SDL_RenderDrawLine(g->ren, 0, VIEW_H - 72, VIEW_W, VIEW_H - 72); for (int i = 0; i < n; i++) { SDL_Rect r = { btns[i].x, btns[i].y, btns[i].w, btns[i].h }; SDL_SetRenderDrawColor(g->ren, btns[i].enabled ? 50 : 35, btns[i].enabled ? 70 : 40, btns[i].enabled ? 55 : 38, 255); SDL_RenderFillRect(g->ren, &r); SDL_SetRenderDrawColor(g->ren, btns[i].enabled ? 120 : 70, btns[i].enabled ? 200 : 90, btns[i].enabled ? 120 : 70, 255); SDL_RenderDrawRect(g->ren, &r); SDL_Color txt = btns[i].enabled ? (SDL_Color){230,230,230,255} : (SDL_Color){120,120,120,255}; snprintf(g_btn_label[i], 64, "%s", btns[i].label); render_text(&g->spr, g->ren, g->spr.font_small, g_btn_label[i], btns[i].x + 4, btns[i].y + 4, txt); char cost[32]; snprintf(cost, 32, "%d scrap", btns[i].cost); render_text(&g->spr, g->ren, g->spr.font_small, cost, btns[i].x + 4, btns[i].y + btns[i].h - 18, (SDL_Color){230,190,70,255}); /* level marker for research buttons */ if (btns[i].kind == 2) { int lvl = g->upg[g->human].lvl[btns[i].id]; char lvs[32]; if (lvl >= UP_MAX_LVL) snprintf(lvs, 32, "MAX"); else if (g->research_cur[g->human] == btns[i].id) snprintf(lvs, 32, "L%d..%d%%", lvl, (int)(g->research_prog[g->human]*100)); else snprintf(lvs, 32, "L%d", lvl); render_text(&g->spr, g->ren, g->spr.font_small, lvs, btns[i].x + 4, btns[i].y + btns[i].h - 34, (SDL_Color){150,210,255,255}); } } } void render_hud(Game *g) { /* top resource bar */ SDL_SetRenderDrawColor(g->ren, 16, 16, 20, 220); SDL_RenderFillRect(g->ren, &(SDL_Rect){0, 0, VIEW_W, 28}); char buf[128]; snprintf(buf, sizeof buf, "%s - Scrap: %d", faction_name(g->human), g->scrap[g->human]); render_text(&g->spr, g->ren, g->spr.font_med, buf, 10, 4, (SDL_Color){230,230,230,255}); /* selection summary */ int n_units = 0, n_bld = 0; for (int i = 0; i < g->sel_count; i++) { int id = g->selection[i]; if (unit_by_id(g, id)) n_units++; else if (bld_by_id(g, id)) n_bld++; } if (n_units > 0 || n_bld > 0) { snprintf(buf, sizeof buf, "Selected: %d units, %d buildings", n_units, n_bld); render_text(&g->spr, g->ren, g->spr.font_small, buf, VIEW_W - 260, 6, (SDL_Color){200,220,200,255}); } /* hints */ render_text(&g->spr, g->ren, g->spr.font_small, "LMB select/drag RMB move/attack A select-all [B] build Space pause F5 save", 10, VIEW_H - 90, (SDL_Color){150,150,150,255}); /* objective */ const Mission *m = mission_get(g->mission_id); const char *obj = (g->objective == OBJ_SURVIVE) ? "Survive the assault" : (g->objective == OBJ_GATHER) ? "Stockpile scrap" : "Destroy all enemies"; char obuf[128]; if (g->objective == OBJ_SURVIVE) snprintf(obuf, sizeof obuf, "Objective: %s (%.0f/%.0f s)", obj, g->time, m->survive_time); else if (g->objective == OBJ_GATHER) snprintf(obuf, sizeof obuf, "Objective: %s (%d/%d)", obj, g->scrap[g->human], m->scrap_goal); else snprintf(obuf, sizeof obuf, "Objective: %s", obj); render_text(&g->spr, g->ren, g->spr.font_small, obuf, VIEW_W/2 - 90, 6, (SDL_Color){230,230,160,255}); if (g->state == GAME_PAUSED) render_text(&g->spr, g->ren, g->spr.font_big, "PAUSED", VIEW_W/2 - 70, VIEW_H/2 - 20, (SDL_Color){255,255,255,255}); if (g->state == GAME_WON) render_text(&g->spr, g->ren, g->spr.font_big, "VICTORY", VIEW_W/2 - 80, VIEW_H/2 - 20, (SDL_Color){120,255,120,255}); if (g->state == GAME_LOST) render_text(&g->spr, g->ren, g->spr.font_big, "DEFEAT", VIEW_W/2 - 70, VIEW_H/2 - 20, (SDL_Color){255,120,120,255}); if (g->state == GAME_WON || g->state == GAME_LOST) render_text(&g->spr, g->ren, g->spr.font_med, "Press M for mission menu", VIEW_W/2 - 120, VIEW_H/2 + 20, (SDL_Color){220,220,220,255}); } void render_minimap(Game *g) { int mw = 200, mh = 200; int mx = VIEW_W - mw - 10, my = 36; SDL_SetRenderDrawColor(g->ren, 0, 0, 0, 220); SDL_RenderFillRect(g->ren, &(SDL_Rect){mx-2, my-2, mw+4, mh+4}); float sx = (float)mw / MAP_W, sy = (float)mh / MAP_H; for (int ty = 0; ty < MAP_H; ty++) { for (int tx = 0; tx < MAP_W; tx++) { uint8_t fv = g->map.fog[ty*MAP_W + tx]; if (fv == 0) continue; Tile *t = map_tile(&g->map, tx, ty); Uint8 cr=120,cg=104,cb=78; switch (t->terrain) { case T_WATER: cr=44;cg=78;cb=120; break; case T_ROCK: cr=110;cg=104;cb=98; break; case T_SCRAP: cr=200;cg=170;cb=70; break; case T_GRASS: cr=210;cg=170;cb=78; break; /* arid wasteland */ } if (fv == 1) { cr/=2; cg/=2; cb/=2; } SDL_SetRenderDrawColor(g->ren, cr, cg, cb, 255); SDL_RenderDrawPoint(g->ren, mx + (int)(tx*sx), my + (int)(ty*sy)); } } for (int i = 0; i < g->unit_count; i++) { Unit *u = &g->units[i]; if (u->dead) continue; if (u->faction != g->human) { int tx = (int)(u->x/TILE), ty = (int)(u->y/TILE); if (!map_in_bounds(&g->map, tx, ty) || g->map.fog[ty*MAP_W+tx] != 2) continue; } SDL_Color c = faction_color(u->faction); SDL_SetRenderDrawColor(g->ren, c.r, c.g, c.b, 255); SDL_RenderDrawPoint(g->ren, mx + (int)(u->x/MAP_PIX_W*mw), my + (int)(u->y/MAP_PIX_H*mh)); } for (int i = 0; i < g->bld_count; i++) { Building *b = &g->buildings[i]; if (b->dead) continue; if (b->faction != g->human) { int tx = b->tx + b->w/2, ty = b->ty + b->h/2; if (!map_in_bounds(&g->map, tx, ty) || g->map.fog[ty*MAP_W+tx] != 2) continue; } SDL_Color c = faction_color(b->faction); int px = mx + (int)((b->tx*TILE)/MAP_PIX_W*mw); int py = my + (int)((b->ty*TILE)/MAP_PIX_H*mh); SDL_SetRenderDrawColor(g->ren, c.r, c.g, c.b, 255); SDL_RenderFillRect(g->ren, &(SDL_Rect){px, py, 2, 2}); } /* viewport box */ int vx = mx + (int)(g->cam.x / MAP_PIX_W * mw); int vy = my + (int)(g->cam.y / MAP_PIX_H * mh); SDL_SetRenderDrawColor(g->ren, 255, 255, 255, 200); SDL_RenderDrawRect(g->ren, &(SDL_Rect){vx, vy, (int)(VIEW_W/MAP_PIX_W*mw), (int)(VIEW_H/MAP_PIX_H*mh)}); } static void render_wrapped(Game *g, TTF_Font *f, const char *txt, int x, int y, int maxw, SDL_Color c) { /* greedy word wrap */ char buf[1024]; strncpy(buf, txt, sizeof buf - 1); buf[sizeof buf - 1] = 0; char *tok = strtok(buf, " "); char line[256]; line[0] = 0; int ly = y; while (tok) { int cur = (int)strlen(line); int add = (int)strlen(tok) + (cur ? 1 : 0); if (cur + add >= (int)sizeof line || cur + add > maxw/8) { render_text(&g->spr, g->ren, f, line, x, ly, c); ly += 22; line[0] = 0; cur = 0; } if (cur) { strcat(line, " "); strcat(line, tok); } else strcpy(line, tok); tok = strtok(NULL, " "); } if (line[0]) render_text(&g->spr, g->ren, f, line, x, ly, c); } void render_menu(Game *g) { SDL_SetRenderDrawColor(g->ren, 18, 16, 14, 255); SDL_RenderClear(g->ren); render_text(&g->spr, g->ren, g->spr.font_big, "KRUSH, KILL 'N' DESTROY", VIEW_W/2 - 260, 36, (SDL_Color){230,200,90,255}); render_text(&g->spr, g->ren, g->spr.font_med, "Recreated post-apocalyptic RTS - choose a mission:", VIEW_W/2 - 230, 84, (SDL_Color){220,220,220,255}); int n = mission_count(); int lx = 40, lw = 470, rh = 34; for (int i = 0; i < n; i++) { const Mission *m = mission_get(i); int y = 124 + i * rh; int locked = (m->id >= g->unlocked); int sel = (i == g->menu_sel); SDL_Rect r = { lx, y, lw, rh - 4 }; SDL_SetRenderDrawColor(g->ren, sel ? 54 : 40, sel ? 60 : 44, sel ? 48 : 38, 255); SDL_RenderFillRect(g->ren, &r); SDL_SetRenderDrawColor(g->ren, sel ? 230 : 120, sel ? 200 : 200, sel ? 90 : 110, 255); SDL_RenderDrawRect(g->ren, &r); char line[200]; const char *obj = (m->objective == OBJ_SURVIVE) ? "Survive" : (m->objective == OBJ_GATHER) ? "Gather" : "Destroy"; snprintf(line, sizeof line, locked ? "%d. %s [LOCKED]" : "%d. %s (%s, %s)", m->id, m->name, obj, m->diff==DIFF_HARD ? "Hard" : "Normal"); render_text(&g->spr, g->ren, g->spr.font_med, line, lx + 10, y + 4, locked ? (SDL_Color){120,120,120,255} : (SDL_Color){230,230,230,255}); } /* briefing panel for the highlighted mission */ const Mission *m = mission_get(g->menu_sel); int px = lx + lw + 30, pw = VIEW_W - px - 30; render_text(&g->spr, g->ren, g->spr.font_big, m->name, px, 124, (SDL_Color){230,200,90,255}); char meta[256]; snprintf(meta, sizeof meta, "Enemy: %s Difficulty: %s Objective: %s", faction_name(m->enemy), m->diff==DIFF_HARD ? "Hard" : "Normal", m->objective==OBJ_SURVIVE ? "Survive the assault" : m->objective==OBJ_GATHER ? "Stockpile scrap" : "Destroy all enemies"); render_text(&g->spr, g->ren, g->spr.font_small, meta, px, 158, (SDL_Color){180,210,255,255}); render_wrapped(g, g->spr.font_med, m->brief, px, 188, pw, (SDL_Color){220,220,220,255}); char hint[256]; snprintf(hint, sizeof hint, "Up/Down select Enter start [F] side: %s 0-9 jump. Factions: Survivors/Evolved/Series 9.", faction_name(g->human)); render_text(&g->spr, g->ren, g->spr.font_small, hint, lx, VIEW_H - 28, (SDL_Color){150,150,150,255}); SDL_RenderPresent(g->ren); } void render_message_log(Game *g) { if (g->log_count <= 0) return; int x = 12; int y = VIEW_H - 200; /* above the build bar */ int shown = (g->log_count < 6) ? g->log_count : 6; for (int i = 0; i < shown; i++) { int idx = (g->log_next - shown + i + 32) % 32; render_text(&g->spr, g->ren, g->spr.font_small, g->log_msg[idx], x, y + i * 18, (SDL_Color){210, 230, 180, 255}); } } void render_frame(Game *g) { if (g->state == GAME_MENU) { render_menu(g); return; } /* camera shake */ float shx = 0, shy = 0; if (g->cam.shake > 0) { shx = ((float)(rand() % 21) - 10) / 10.0f * g->cam.shake; shy = ((float)(rand() % 21) - 10) / 10.0f * g->cam.shake; g->cam.x += shx; g->cam.y += shy; } SDL_SetRenderDrawColor(g->ren, 0, 0, 0, 255); SDL_RenderClear(g->ren); render_world(g); render_wrecks(g); render_buildings(g); render_units(g); render_bullets(g); render_particles(g); render_selection(g); render_build_menu(g); render_hud(g); render_minimap(g); render_message_log(g); SDL_RenderPresent(g->ren); if (g->cam.shake > 0) { g->cam.x -= shx; g->cam.y -= shy; } }