kitchen: README + MISSIONS/engine-c keep-drop notes

This commit is contained in:
Hernâni Marques 2026-09-17 21:48:48 +02:00
parent 1d351918da
commit 575d7d29b8
No known key found for this signature in database
130 changed files with 9817 additions and 1 deletions

631
engine-c/src/render.c Normal file
View file

@ -0,0 +1,631 @@
#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: draw then scaffold overlay */
SDL_SetTextureAlphaMod(tex, 160);
SDL_RenderCopy(g->ren, tex, NULL, &r);
SDL_SetTextureAlphaMod(tex, 255);
int pct = (int)(b->build * 100);
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);
}
/* health bar if damaged or selected */
if (b->hp < b->max_hp || b->selected) {
SDL_Rect bar = { sx, sy - 6, W, 4 };
SDL_SetRenderDrawColor(g->ren, 60, 60, 60, 255);
SDL_RenderFillRect(g->ren, &bar);
float f = b->hp / b->max_hp;
SDL_Color c = (b->faction == FACTION_SURVIVOR) ? (SDL_Color){90,160,70,255}
: (SDL_Color){170,70,180,255};
SDL_Rect fill = { sx, sy - 6, (int)(W * f), 4 };
SDL_SetRenderDrawColor(g->ren, c.r, c.g, c.b, 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 });
}
}
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 = 28;
SDL_Texture *tex = g->spr.unit[u->type][u->faction];
draw_tex_world(g, tex, u->x, u->y, sz, sz, u->heading, 255);
/* selection ring */
if (u->selected) {
int cx = (int)(u->x - g->cam.x), cy = (int)(u->y - g->cam.y);
SDL_SetRenderDrawColor(g->ren, 120, 255, 120, 220);
for (int a = 0; a < 360; a += 12) {
int px = cx + (int)(cosf(a*3.14159f/180)*16);
int py = cy + (int)(sinf(a*3.14159f/180)*16);
SDL_RenderDrawPoint(g->ren, px, py);
}
}
/* health bar */
if (u->hp < u->max_hp || u->selected) {
int cx = (int)(u->x - g->cam.x), by = (int)(u->y - g->cam.y) - 18;
int bw = 24;
SDL_Rect bg = { cx - bw/2, by, bw, 4 };
SDL_SetRenderDrawColor(g->ren, 50, 50, 50, 255);
SDL_RenderFillRect(g->ren, &bg);
float f = u->hp / u->max_hp;
SDL_Color c = (u->faction == FACTION_SURVIVOR) ? (SDL_Color){90,200,90,255}
: (SDL_Color){220,110,220,255};
SDL_Rect fg = { cx - bw/2, by, (int)(bw * f), 4 };
SDL_SetRenderDrawColor(g->ren, c.r, c.g, c.b, 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) {
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);
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);
}
}
void render_particles(Game *g) {
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);
Uint8 a = (Uint8)(255 * (p->life / p->max_life));
SDL_SetRenderDrawColor(g->ren, p->r, p->g, p->b, a);
int sz = (p->kind == 1) ? 3 : 2;
SDL_RenderFillRect(g->ren, &(SDL_Rect){ sx-sz/2, sy-sz/2, sz, sz });
}
}
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=86;cg=120;cb=64; break;
}
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; }
}