OpenKKnD/engine-c/src/render.c
2026-09-19 13:03:33 +02:00

885 lines
39 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#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; }
}