From d983fbb0d7ea37b3ccca86a8540eb19966d14c5c Mon Sep 17 00:00:00 2001 From: "local-model/engine-L2-grok-4.5" Date: Sat, 19 Sep 2026 13:32:48 +0200 Subject: [PATCH] engine-c: richer snapshot/live HUD (scrap/power/minimap/command strip) --- engine-c/src/render.c | 146 +++++++++++++++++++++++++--- engine-c/src/snapshot.c | 204 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 325 insertions(+), 25 deletions(-) diff --git a/engine-c/src/render.c b/engine-c/src/render.c index d85dd5f..5a989d0 100644 --- a/engine-c/src/render.c +++ b/engine-c/src/render.c @@ -650,16 +650,79 @@ void render_build_menu(Game *g) { } 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}); + /* KKND2-like chrome: top scrap/power bars + bottom command strip */ + Faction hf = g->human; + SDL_Color hc = faction_color(hf); + SDL_Color ec = faction_color(enemy_of(hf)); - /* selection summary */ + int scrap_h = g->scrap[hf]; + if (scrap_h < 0) scrap_h = 0; + int scrap_cap = 8000; + if (scrap_h > scrap_cap) scrap_cap = scrap_h + 500; + float scrap_frac = (float)scrap_h / (float)scrap_cap; + if (scrap_frac > 1.0f) scrap_frac = 1.0f; + + int pwr_gen = 0, pwr_use = 0; + for (int i = 0; i < g->bld_count; i++) { + Building *b = &g->buildings[i]; + if (b->dead || b->faction != hf || b->build < 1.0f) continue; + if (b->type == BT_POWER) pwr_gen += 100; + else if (b->type == BT_BASE) pwr_use += 20; + else if (b->type == BT_WARFACTORY || b->type == BT_REFINERY) pwr_use += 40; + else if (b->type == BT_RESEARCH || b->type == BT_REPAIR) pwr_use += 30; + else if (b->type == BT_TURRET) pwr_use += 15; + else pwr_use += 5; + } + if (pwr_gen < 1) pwr_gen = 1; + float pwr_frac = (float)pwr_gen / (float)(pwr_gen + pwr_use); + if (pwr_frac > 1.0f) pwr_frac = 1.0f; + int pwr_ok = (pwr_gen >= pwr_use); + + int bar_h = 28; + SDL_SetRenderDrawColor(g->ren, 22, 26, 32, 230); + SDL_RenderFillRect(g->ren, &(SDL_Rect){0, 0, VIEW_W, bar_h}); + SDL_SetRenderDrawColor(g->ren, 55, 62, 70, 255); + SDL_RenderFillRect(g->ren, &(SDL_Rect){0, 0, VIEW_W, 2}); + SDL_SetRenderDrawColor(g->ren, 90, 110, 70, 255); + SDL_RenderFillRect(g->ren, &(SDL_Rect){0, bar_h - 1, VIEW_W, 1}); + + /* faction badge */ + SDL_SetRenderDrawColor(g->ren, 12, 14, 16, 255); + SDL_RenderFillRect(g->ren, &(SDL_Rect){4, 4, 20, 20}); + SDL_SetRenderDrawColor(g->ren, hc.r, hc.g, hc.b, 255); + SDL_RenderFillRect(g->ren, &(SDL_Rect){6, 6, 16, 16}); + + /* scrap bar */ + int scrap_x = 48, scrap_w = (int)(VIEW_W * 0.22f); + if (scrap_w < 80) scrap_w = 80; + SDL_SetRenderDrawColor(g->ren, 28, 26, 18, 255); + SDL_RenderFillRect(g->ren, &(SDL_Rect){scrap_x, 8, scrap_w, 12}); + SDL_SetRenderDrawColor(g->ren, 230, 200, 70, 255); + SDL_RenderFillRect(g->ren, &(SDL_Rect){scrap_x, 8, (int)(scrap_w * scrap_frac), 12}); + for (int s = 1; s < 8; s++) { + int sx = scrap_x + (scrap_w * s) / 8; + SDL_SetRenderDrawColor(g->ren, 18, 16, 12, 255); + SDL_RenderDrawLine(g->ren, sx, 8, sx, 19); + } + + /* power bar */ + int pwr_bx = scrap_x + scrap_w + 16, pwr_bw = (int)(VIEW_W * 0.14f); + if (pwr_bw < 50) pwr_bw = 50; + SDL_SetRenderDrawColor(g->ren, 24, 28, 24, 255); + SDL_RenderFillRect(g->ren, &(SDL_Rect){pwr_bx, 8, pwr_bw, 12}); + if (pwr_ok) SDL_SetRenderDrawColor(g->ren, 70, 210, 90, 255); + else SDL_SetRenderDrawColor(g->ren, 220, 70, 60, 255); + SDL_RenderFillRect(g->ren, &(SDL_Rect){pwr_bx, 8, (int)(pwr_bw * pwr_frac), 12}); + + char buf[128]; + snprintf(buf, sizeof buf, "Scrap %d", scrap_h); + render_text(&g->spr, g->ren, g->spr.font_small, buf, scrap_x + 4, 8, + (SDL_Color){255,245,200,255}); + snprintf(buf, sizeof buf, "Pwr %d/%d", pwr_gen, pwr_use); + render_text(&g->spr, g->ren, g->spr.font_small, buf, pwr_bx + 4, 8, + (SDL_Color){220,255,220,255}); + + /* selection summary + threat pip */ int n_units = 0, n_bld = 0; for (int i = 0; i < g->sel_count; i++) { int id = g->selection[i]; @@ -667,16 +730,69 @@ void render_hud(Game *g) { 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); + snprintf(buf, sizeof buf, "Sel %dU/%dB", 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}); + VIEW_W - 280, 6, (SDL_Color){200,220,200,255}); + } + { + int eth = 0; + for (int i = 0; i < g->unit_count; i++) + if (!g->units[i].dead && g->units[i].faction != hf) eth++; + if (eth > 36) eth = 36; + SDL_SetRenderDrawColor(g->ren, 36, 22, 28, 255); + SDL_RenderFillRect(g->ren, &(SDL_Rect){VIEW_W - 56, 5, 48, 18}); + SDL_SetRenderDrawColor(g->ren, ec.r, ec.g, ec.b, 255); + SDL_RenderFillRect(g->ren, &(SDL_Rect){VIEW_W - 52, 8, 8, 12}); + SDL_RenderFillRect(g->ren, &(SDL_Rect){VIEW_W - 40, 10, eth, 8}); } - /* hints */ + /* bottom command strip */ + int bot_h = 36, bot_y = VIEW_H - bot_h; + SDL_SetRenderDrawColor(g->ren, 18, 20, 24, 230); + SDL_RenderFillRect(g->ren, &(SDL_Rect){0, bot_y, VIEW_W, bot_h}); + SDL_SetRenderDrawColor(g->ren, 70, 90, 70, 255); + SDL_RenderFillRect(g->ren, &(SDL_Rect){0, bot_y, VIEW_W, 1}); + SDL_SetRenderDrawColor(g->ren, 12, 14, 16, 255); + SDL_RenderFillRect(g->ren, &(SDL_Rect){6, bot_y + 4, 28, 28}); + SDL_SetRenderDrawColor(g->ren, hc.r, hc.g, hc.b, 255); + SDL_RenderFillRect(g->ren, &(SDL_Rect){8, bot_y + 6, 24, 24}); + int slots = (n_units > 0 || n_bld > 0) ? (n_units + n_bld) : 6; + if (slots > 10) slots = 10; + for (int s = 0; s < slots; s++) { + int sx = 42 + s * 28; + SDL_SetRenderDrawColor(g->ren, 28, 32, 28, 255); + SDL_RenderFillRect(g->ren, &(SDL_Rect){sx, bot_y + 4, 24, 28}); + if (s < n_units || (n_units == 0 && n_bld == 0 && s < 4)) { + SDL_SetRenderDrawColor(g->ren, hc.r, hc.g, hc.b, 255); + SDL_RenderFillRect(g->ren, &(SDL_Rect){sx + 5, bot_y + 9, 14, 16}); + } else if (s < n_units + n_bld) { + SDL_SetRenderDrawColor(g->ren, hc.r, hc.g, hc.b, 255); + SDL_RenderFillRect(g->ren, &(SDL_Rect){sx + 4, bot_y + 8, 16, 16}); + } + } + for (int s = 0; s < 4; s++) { + int sx = VIEW_W - 120 + s * 28; + SDL_SetRenderDrawColor(g->ren, 32, 28, 22, 255); + SDL_RenderFillRect(g->ren, &(SDL_Rect){sx, bot_y + 4, 24, 28}); + if (s == 0) { + SDL_SetRenderDrawColor(g->ren, 200, 180, 60, 255); + SDL_RenderFillRect(g->ren, &(SDL_Rect){sx + 8, bot_y + 12, 8, 10}); + } else if (s == 1) { + SDL_SetRenderDrawColor(g->ren, 255, 80, 40, 255); + SDL_RenderFillRect(g->ren, &(SDL_Rect){sx + 8, bot_y + 12, 8, 8}); + } else if (s == 2) { + SDL_SetRenderDrawColor(g->ren, 80, 180, 255, 255); + SDL_RenderFillRect(g->ren, &(SDL_Rect){sx + 6, bot_y + 14, 12, 6}); + } else { + SDL_SetRenderDrawColor(g->ren, 180, 255, 120, 255); + SDL_RenderDrawRect(g->ren, &(SDL_Rect){sx + 6, bot_y + 10, 12, 12}); + } + } + + /* hints above command strip */ 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}); + 10, bot_y - 18, (SDL_Color){150,150,150,255}); /* objective */ const Mission *m = mission_get(g->mission_id); @@ -693,7 +809,7 @@ void render_hud(Game *g) { 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}); + pwr_bx + pwr_bw + 16, 6, (SDL_Color){230,230,160,255}); if (g->state == GAME_PAUSED) render_text(&g->spr, g->ren, g->spr.font_big, "PAUSED", diff --git a/engine-c/src/snapshot.c b/engine-c/src/snapshot.c index 0f042b3..dbd46dc 100644 --- a/engine-c/src/snapshot.c +++ b/engine-c/src/snapshot.c @@ -736,17 +736,201 @@ void snapshot_capture(Game *g, const char *path) { if (p->kind == 0 || p->kind == 1) disc(sx, sy, sz + 1, r, gv, b); else fill_rect(sx - sz/2, sy - sz/2, sz + 1, sz + 1, r, gv, b); } - /* No shot minimap — keeps the crop on the fight plate. */ + /* KKND2-like chrome: top resource bar + corner minimap + bottom command strip. + * Procedural bars (no font) so the shot still reads as classic RTS UI. */ + { + Faction hf = g->human; + uint8_t hr = faction_r(hf), hg = faction_g(hf), hb = faction_b(hf); + Faction ef = enemy_of(hf); + uint8_t er = faction_r(ef), eg = faction_g(ef), eb = faction_b(ef); - /* Simple top HUD strip (resource/faction cue like classic RTS). */ - fill_rect(0, 0, g_sw, 14, 18, 22, 28); - fill_rect(0, 13, g_sw, 1, 70, 90, 70); - fill_rect(4, 3, 28, 8, 40, 210, 70); - fill_rect(36, 3, 28, 8, 235, 55, 235); - fill_rect(68, 4, (int)(g_sw * 0.22f), 6, 200, 180, 60); - fill_rect(68, 4, (int)(g_sw * 0.14f), 6, 255, 220, 80); - disc(g_sw - 18, 7, 5, 255, 90, 40); - disc(g_sw - 18, 7, 2, 255, 220, 120); + int scrap_h = g->scrap[hf]; + if (scrap_h < 0) scrap_h = 0; + int scrap_cap = 8000; + if (scrap_h > scrap_cap) scrap_cap = scrap_h + 500; + float scrap_frac = (float)scrap_h / (float)scrap_cap; + if (scrap_frac > 1.0f) scrap_frac = 1.0f; + + int pwr_gen = 0, pwr_use = 0; + for (int i = 0; i < g->bld_count; i++) { + Building *b = &g->buildings[i]; + if (b->dead || b->faction != hf || b->build < 1.0f) continue; + if (b->type == BT_POWER) pwr_gen += 100; + else if (b->type == BT_BASE) pwr_use += 20; + else if (b->type == BT_WARFACTORY || b->type == BT_REFINERY) pwr_use += 40; + else if (b->type == BT_RESEARCH || b->type == BT_REPAIR) pwr_use += 30; + else if (b->type == BT_TURRET) pwr_use += 15; + else pwr_use += 5; + } + if (pwr_gen < 1) pwr_gen = 1; + float pwr_frac = (float)pwr_gen / (float)(pwr_gen + pwr_use); + if (pwr_frac > 1.0f) pwr_frac = 1.0f; + int pwr_ok = (pwr_gen >= pwr_use); + + int n_sel_u = 0, n_sel_b = 0; + for (int i = 0; i < g->sel_count; i++) { + if (unit_by_id(g, g->selection[i])) n_sel_u++; + else if (bld_by_id(g, g->selection[i])) n_sel_b++; + } + + /* —— top bar —— */ + int bar_h = 22; + fill_rect(0, 0, g_sw, bar_h, 22, 26, 32); + fill_rect(0, 0, g_sw, 2, 55, 62, 70); + fill_rect(0, bar_h - 1, g_sw, 1, 90, 110, 70); + fill_rect(0, bar_h, g_sw, 1, 40, 48, 40); + + /* faction badge */ + fill_rect(3, 3, 16, 16, 12, 14, 16); + fill_rect(5, 5, 12, 12, hr, hg, hb); + fill_rect(7, 7, 8, 8, (uint8_t)(hr / 2 + 40), (uint8_t)(hg / 2 + 40), + (uint8_t)(hb / 2 + 40)); + + /* scrap icon (gold nugget) + segmented bar */ + fill_rect(24, 5, 8, 12, 40, 34, 18); + fill_rect(26, 7, 4, 8, 220, 190, 70); + disc(28, 11, 3, 255, 220, 90); + int scrap_x = 36, scrap_w = (int)(g_sw * 0.28f); + if (scrap_w < 60) scrap_w = 60; + fill_rect(scrap_x, 6, scrap_w, 10, 28, 26, 18); + fill_rect(scrap_x, 6, (int)(scrap_w * scrap_frac), 10, 230, 200, 70); + /* segment ticks */ + for (int s = 1; s < 8; s++) { + int sx = scrap_x + (scrap_w * s) / 8; + line(sx, 6, sx, 15, 18, 16, 12); + } + fill_rect(scrap_x, 6, scrap_w, 1, 255, 240, 160); + fill_rect(scrap_x, 15, scrap_w, 1, 80, 60, 20); + + /* power bolt + bar (green surplus / red deficit) */ + int pwr_x = scrap_x + scrap_w + 10; + fill_rect(pwr_x, 5, 3, 5, 240, 240, 120); + fill_rect(pwr_x - 1, 9, 5, 3, 240, 240, 120); + fill_rect(pwr_x + 1, 11, 3, 5, 240, 240, 120); + int pwr_bx = pwr_x + 10, pwr_bw = (int)(g_sw * 0.16f); + if (pwr_bw < 40) pwr_bw = 40; + fill_rect(pwr_bx, 6, pwr_bw, 10, 24, 28, 24); + uint8_t pr = pwr_ok ? 70 : 220, pg = pwr_ok ? 210 : 70, pb = pwr_ok ? 90 : 60; + fill_rect(pwr_bx, 6, (int)(pwr_bw * pwr_frac), 10, pr, pg, pb); + for (int s = 1; s < 5; s++) { + int sx = pwr_bx + (pwr_bw * s) / 5; + line(sx, 6, sx, 15, 14, 16, 14); + } + + /* unit / building count chips */ + int chip_x = pwr_bx + pwr_bw + 12; + fill_rect(chip_x, 4, 34, 14, 30, 36, 30); + fill_rect(chip_x + 2, 6, 6, 10, hr, hg, hb); /* unit glyph */ + disc(chip_x + 5, 8, 2, 255, 255, 255); + int ufill = alive_h; + if (ufill > 20) ufill = 20; + fill_rect(chip_x + 12, 8, ufill, 6, hr, hg, hb); + fill_rect(chip_x + 38, 4, 34, 14, 30, 36, 30); + fill_rect(chip_x + 40, 7, 8, 8, hr, hg, hb); /* bld glyph */ + int bfill = 0; + for (int i = 0; i < g->bld_count; i++) + if (!g->buildings[i].dead && g->buildings[i].faction == hf) bfill++; + if (bfill > 20) bfill = 20; + fill_rect(chip_x + 52, 8, bfill, 6, 180, 160, 70); + + /* enemy threat pip */ + fill_rect(g_sw - 48, 4, 42, 14, 36, 22, 28); + fill_rect(g_sw - 44, 6, 8, 10, er, eg, eb); + int eth = alive_e; + if (eth > 28) eth = 28; + fill_rect(g_sw - 34, 8, eth, 6, er, eg, eb); + disc(g_sw - 10, 11, 4, 255, 90, 40); + disc(g_sw - 10, 11, 2, 255, 220, 120); + + /* —— corner minimap (compact, fight-plate friendly) —— */ + int mw = 72, mh = 72; + int mx = g_sw - mw - 6, my = bar_h + 6; + fill_rect(mx - 2, my - 2, mw + 4, mh + 4, 8, 10, 12); + fill_rect(mx - 1, my - 1, mw + 2, mh + 2, 70, 90, 70); + fill_rect(mx, my, mw, mh, 28, 32, 28); + /* sample terrain around camera */ + float map_sx = (float)mw / (float)g_sw; + float map_sy = (float)mh / (float)g_sh; + for (int py = 0; py < mh; py += 2) { + for (int px_ = 0; px_ < mw; px_ += 2) { + float wx = camx + (float)px_ / map_sx; + float wy = camy + (float)py / map_sy; + int tx = (int)(wx / TILE), ty = (int)(wy / TILE); + if (!map_in_bounds(&g->map, tx, ty)) continue; + Tile *t = map_tile(&g->map, tx, ty); + uint8_t 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; + case T_RUBBLE: cr = 90; cg = 78; cb = 60; break; + default: break; + } + fill_rect(mx + px_, my + py, 2, 2, cr, cg, cb); + } + } + for (int i = 0; i < g->bld_count; i++) { + Building *b = &g->buildings[i]; + if (b->dead) continue; + int bx = mx + (int)((b->tx * TILE - camx) * map_sx); + int by = my + (int)((b->ty * TILE - camy) * map_sy); + if (bx < mx || by < my || bx >= mx + mw || by >= my + mh) continue; + uint8_t br = faction_r(b->faction), bg = faction_g(b->faction), + bb = faction_b(b->faction); + fill_rect(bx, by, 3, 3, br, bg, bb); + } + for (int i = 0; i < g->unit_count; i++) { + Unit *u = &g->units[i]; + if (u->dead) continue; + int ux = mx + (int)((u->x - camx) * map_sx); + int uy = my + (int)((u->y - camy) * map_sy); + if (ux < mx || uy < my || ux >= mx + mw || uy >= my + mh) continue; + px(ux, uy, faction_r(u->faction), faction_g(u->faction), + faction_b(u->faction)); + } + /* viewport outline = full minimap (crop view) */ + line(mx, my, mx + mw - 1, my, 255, 255, 255); + line(mx, my + mh - 1, mx + mw - 1, my + mh - 1, 255, 255, 255); + line(mx, my, mx, my + mh - 1, 255, 255, 255); + line(mx + mw - 1, my, mx + mw - 1, my + mh - 1, 255, 255, 255); + + /* —— bottom command / selection strip —— */ + int bot_h = 28; + int bot_y = g_sh - bot_h; + fill_rect(0, bot_y, g_sw, bot_h, 18, 20, 24); + fill_rect(0, bot_y, g_sw, 1, 70, 90, 70); + fill_rect(0, bot_y + 1, g_sw, 1, 40, 48, 40); + /* portrait panel */ + fill_rect(4, bot_y + 3, 22, 22, 12, 14, 16); + fill_rect(6, bot_y + 5, 18, 18, hr, hg, hb); + disc(15, bot_y + 12, 4, 255, 255, 255); + fill_rect(13, bot_y + 16, 4, 5, 40, 40, 40); + /* selection count bars */ + int slots = (n_sel_u > 0 || n_sel_b > 0) ? (n_sel_u + n_sel_b) : 6; + if (slots > 10) slots = 10; + for (int s = 0; s < slots; s++) { + int sx = 32 + s * 22; + fill_rect(sx, bot_y + 4, 18, 20, 28, 32, 28); + fill_rect(sx + 1, bot_y + 5, 16, 18, 40, 48, 40); + if (s < n_sel_u || (n_sel_u == 0 && n_sel_b == 0 && s < 4)) { + fill_rect(sx + 4, bot_y + 8, 10, 12, hr, hg, hb); + disc(sx + 9, bot_y + 11, 2, 255, 255, 255); + } else if (s < n_sel_u + n_sel_b) { + fill_rect(sx + 3, bot_y + 8, 12, 12, hr, hg, hb); + } + } + /* ability / build button stubs (right) */ + for (int s = 0; s < 4; s++) { + int sx = g_sw - 96 + s * 22; + fill_rect(sx, bot_y + 4, 18, 20, 32, 28, 22); + fill_rect(sx + 2, bot_y + 6, 14, 16, 70, 60, 40); + if (s == 0) fill_rect(sx + 6, bot_y + 10, 6, 8, 200, 180, 60); /* build */ + if (s == 1) disc(sx + 9, bot_y + 14, 4, 255, 80, 40); /* attack */ + if (s == 2) fill_rect(sx + 5, bot_y + 11, 8, 6, 80, 180, 255); /* move */ + if (s == 3) ring(sx + 9, bot_y + 14, 5, 2, 180, 255, 120); /* stop */ + } + } /* Nearest-neighbour upscale → full VIEW (chunky KKND pixels) */ uint8_t *big = (uint8_t *)malloc((size_t)SW * SH * 4);