885 lines
39 KiB
C
885 lines
39 KiB
C
#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; }
|
||
}
|