juegos

Juegos de terminal de Pancho: Catan (TUI, GUI, web, servidor, PicoCalc), ajedrez, calculadora y minijuegos
git clone https://git.lu3dhn.xyz/juegos.git
Log | Files | Refs

screen.c (42545B)


      1 /* screen.c - pantalla completa en pixeles: layout ancho (PC 640x360+), vertical (la
      2  * web en un telefono: tablero arriba, panel abajo) y compacto (PicoCalc 320x320).
      3  * Registra las zonas clickeables en un HitList. */
      4 #include <string.h>
      5 #include "catan_gfx.h"
      6 
      7 /* ------------------------------------------------------------ utilidades */
      8 
      9 static void cat(char *buf, int n, const char *s) { int p = (int)strlen(buf); str_put(buf, n, p, s); }
     10 static void cati(char *buf, int n, long v) { int p = (int)strlen(buf); str_int(buf, n, p, v); }
     11 
     12 static void box(Surface *s, int x, int y, int w, int h, Color bg, Color border)
     13 {
     14     gfx_rect(s, x, y, w, h, bg);
     15     gfx_frame(s, x, y, w, h, border);
     16 }
     17 
     18 static int center_text(Surface *s, const Font *f, int x, int y, int w, const char *t, Color c)
     19 {
     20     int tw = gfx_text_w(f, t);
     21     if (tw > w) return gfx_text_fit(s, f, x, y, w, t, c);
     22     return gfx_text(s, f, x + (w - tw) / 2, y, t, c);
     23 }
     24 
     25 static Color dim(Color c)
     26 {
     27     int r = (c >> 11) & 31, g = (c >> 5) & 63, b = c & 31;
     28     return (Color)(((r / 2) << 11) | ((g / 2) << 5) | (b / 2));
     29 }
     30 
     31 static void res_card(Surface *s, int x, int y, int w, int h, int r, int n, HitList *hits)
     32 {
     33     Color bg = n ? TERRAIN_COLOR[r] : dim(TERRAIN_COLOR[r]);
     34     box(s, x, y, w, h, bg, C_OUTLINE);
     35     center_text(s, &font_small, x + 1, y + 2, w - 2, res_short(r), C_OUTLINE);
     36     char num[4] = { 0 };
     37     str_int(num, sizeof num, 0, n);
     38     center_text(s, &font_bold, x, y + h - 14, w, num, n ? C_OUTLINE : RGB(60, 60, 60));
     39     (void)hits;
     40 }
     41 
     42 /* ------------------------------------------------------------ piezas comunes */
     43 
     44 /* costo de una accion de construir (NULL si no construye nada) */
     45 static const uint8_t *action_cost(int action)
     46 {
     47     switch (action) {
     48     case A_ROAD: return COST_ROAD;
     49     case A_SETTLE: return COST_SETTLE;
     50     case A_CITY: return COST_CITY;
     51     case A_BUYDEV: return COST_DEV;
     52     case A_SHIP: return COST_SHIP;
     53     }
     54     return 0;
     55 }
     56 
     57 /* los cuadraditos del costo, alineados a la derecha (termina en x1); devuelve el ancho */
     58 static int cost_squares(Surface *s, int x1, int y, const uint8_t *cost, int sq)
     59 {
     60     int n = 0;
     61     for (int r = 0; r < NRES; r++) n += cost[r];
     62     int w = n * (sq + 2) - 2, x = x1 - w;
     63     if (s)
     64         for (int r = 0; r < NRES; r++)
     65             for (int k = 0; k < cost[r]; k++) { gfx_rect(s, x, y, sq, sq + 1, TERRAIN_COLOR[r]); gfx_frame(s, x, y, sq, sq + 1, C_OUTLINE); x += sq + 2; }
     66     return w;
     67 }
     68 
     69 static void top_bar(Surface *s, const Game *g, const UiInfo *ui, int w, int h, bool compact, HitList *hits)
     70 {
     71     gfx_rect(s, 0, 0, w, h, C_ACCENT);
     72     int menu = 0;
     73     if (ui->touch && !compact && !ui->preview) {   /* tactil: "menu" (ayuda, salir) a la derecha */
     74         menu = 22;
     75         int mx = w - 19;
     76         gfx_rect(s, mx, 1, 17, h - 2, C_OUTLINE);
     77         for (int i = 0; i < 3; i++) gfx_rect(s, mx + 4, 4 + i * 3, 9, 1, C_ACCENT);
     78         hit_add(hits, w - 24, 0, 24, h, PK_APP, 12);
     79     }
     80     const Font *f = compact ? &font_small : &font_bold;
     81     int x = 3 + gfx_text(s, f, 3, (h - f->h) / 2 + 1, "CATAN", C_OUTLINE) + 8;
     82     char buf[96] = "";
     83     cat(buf, sizeof buf, g_lang ? "Turn " : "Turno ");
     84     cati(buf, sizeof buf, g->turn + 1);
     85     if (g->dice[0]) {
     86         cat(buf, sizeof buf, "  ");
     87         cati(buf, sizeof buf, g->dice[0]); cat(buf, sizeof buf, "+"); cati(buf, sizeof buf, g->dice[1]);
     88         if (g->dice[2]) { cat(buf, sizeof buf, " "); cati(buf, sizeof buf, g->dice[2]); cat(buf, sizeof buf, "+"); cati(buf, sizeof buf, g->dice[3]); }
     89     }
     90     if (g->winner >= 0 && g->phase != PH_OVER) {     /* juego libre */
     91         cat(buf, sizeof buf, "  * ");
     92         cat(buf, sizeof buf, ui->names[g->winner]);
     93         cat(buf, sizeof buf, " ");
     94         cat(buf, sizeof buf, tr(S_WON_FREE));
     95     }
     96     gfx_text(s, compact ? &font_small : &font_normal, x, (h - (compact ? 7 : 12)) / 2 + 1, buf, C_OUTLINE);
     97     /* banco */
     98     int bw = compact ? 14 : 20, bx = w - NRES * (bw + 1) - 2 - menu;
     99     for (int r = 0; r < NRES; r++) {
    100         char n[4] = { 0 };
    101         str_int(n, sizeof n, 0, g->bank[r]);
    102         gfx_rect(s, bx + r * (bw + 1), 1, bw, h - 2, TERRAIN_COLOR[r]);
    103         center_text(s, &font_small, bx + r * (bw + 1), (h - 7) / 2, bw, n, C_OUTLINE);
    104     }
    105     if (!compact) {
    106         char d[16] = "";
    107         cat(d, sizeof d, tr(S_DECK)); cat(d, sizeof d, " "); cati(d, sizeof d, g->deck_n);
    108         gfx_text(s, &font_normal, bx - gfx_text_w(&font_normal, d) - 8, (h - 12) / 2 + 1, d, C_OUTLINE);
    109     }
    110 }
    111 
    112 /* rowh: alto de la fila con detalle (27; 21 con 5-6 jugadores, para que entren todos) */
    113 static void player_line(Surface *s, const Game *g, const UiInfo *ui, int p, int x, int y, int w, bool detail, int rowh)
    114 {
    115     const Player *pl = &g->p[p];
    116     int sy = rowh >= 27 ? 13 : 11;          /* donde va la linea chica */
    117     if (p == g->cur && g->phase != PH_OVER) gfx_rect(s, x, y - 1, w, detail ? rowh - 1 : 13, C_PANEL2);
    118     gfx_rect(s, x + 2, y + 1, 9, 9, C_OUTLINE);
    119     gfx_rect(s, x + 3, y + 2, 7, 7, PLAYER_COLOR[p]);
    120     Color tc = pl->neutral ? C_DIM : C_TEXT;
    121     int name_w = w - 70;            /* lo que deja libre la derecha (fichas y PV) */
    122     char buf[64] = "";
    123     if (pl->neutral) {
    124         cat(buf, sizeof buf, tr(S_NEUTRAL));
    125         gfx_text(s, &font_small, x + w - gfx_text_w(&font_small, buf) - 3, y + 2, buf, C_DIM);
    126         gfx_text_fit(s, &font_normal, x + 14, y - 1, name_w, ui->names[p], tc);
    127         return;
    128     }
    129     int vp = game_vp_public(g, p);
    130     int hid = (p == ui->view_seat || g->phase == PH_OVER) ? pl->dev[D_VP] + pl->dev_new[D_VP] : 0;
    131     cati(buf, sizeof buf, vp);
    132     if (hid) { cat(buf, sizeof buf, "+"); cati(buf, sizeof buf, hid); }
    133     cat(buf, sizeof buf, " ");
    134     cat(buf, sizeof buf, tr(S_VP));
    135     int vx = x + w - gfx_text_w(&font_bold, buf) - 3;
    136     gfx_text(s, &font_bold, vx, y - 1, buf, C_ACCENT);
    137     /* fichas que le quedan (publico, como las pilas en la mesa): pueblos, ciudades, rutas */
    138     int px = vx - 8;
    139     const int left[4] = { pl->settles, pl->cities, pl->roads, pl->ships };
    140     for (int k = game_has_sea(g) ? 3 : 2; k >= 0; k--) {     /* con mar, tambien los barcos */
    141         char nb[4] = "";
    142         cati(nb, sizeof nb, left[k]);
    143         px -= gfx_text_w(&font_normal, nb);
    144         gfx_text(s, &font_normal, px, y - 1, nb, left[k] ? C_TEXT : C_ERR);
    145         int iw = k == 1 || k == 3 ? 15 : 11;   /* la ciudad y el barco son mas anchos */
    146         px -= iw + 1;
    147         gfx_piece(s, px + iw / 2, y + 5, 4, k, PLAYER_COLOR[p]);
    148         px -= 5;
    149     }
    150     name_w = px - (x + 14) - 2;
    151     gfx_text_fit(s, p == ui->view_seat ? &font_bold : &font_normal, x + 14, y - 1, name_w, ui->names[p], tc);
    152     if (!detail) return;
    153     int devs = pl->dev_hidden;
    154     for (int d = 0; d < NDEV; d++) devs += pl->dev[d] + pl->dev_new[d];
    155     buf[0] = 0;
    156     int n = hand_size(g, p);
    157     cati(buf, sizeof buf, n); cat(buf, sizeof buf, " "); cat(buf, sizeof buf, tr(n == 1 ? S_CARD1 : S_CARDS));
    158     cat(buf, sizeof buf, " "); cati(buf, sizeof buf, devs); cat(buf, sizeof buf, g_lang ? " dev " : " des ");
    159     cati(buf, sizeof buf, pl->knights); cat(buf, sizeof buf, g_lang ? " kn " : " cab ");
    160     cat(buf, sizeof buf, g_lang ? "road " : "ruta "); cati(buf, sizeof buf, pl->road_len);
    161     if (g->o.variant2p) { cat(buf, sizeof buf, "  "); cati(buf, sizeof buf, pl->tokens); cat(buf, sizeof buf, "F"); }
    162     int tx = x + 14 + gfx_text_fit(s, &font_small, x + 14, y + sy, w - 50, buf, C_DIM) + 4;
    163     if (g->longest == p) { gfx_rect(s, tx, y + sy - 1, 13, 9, C_ACCENT); gfx_text(s, &font_small, tx + 1, y + sy, "LR", C_OUTLINE); tx += 16; }
    164     if (g->army == p) { gfx_rect(s, tx, y + sy - 1, 13, 9, C_ERR); gfx_text(s, &font_small, tx + 1, y + sy, "EJ", C_OUTLINE); }
    165 }
    166 
    167 static void dev_list(Surface *s, const Game *g, int p, int x, int y, int w)
    168 {
    169     char buf[128] = "";
    170     const Player *pl = &g->p[p];
    171     for (int d = 0; d < NDEV; d++) {
    172         if (!pl->dev[d] && !pl->dev_new[d]) continue;
    173         if (buf[0]) cat(buf, sizeof buf, ", ");
    174         cat(buf, sizeof buf, dev_name(d));
    175         cat(buf, sizeof buf, " x");
    176         cati(buf, sizeof buf, pl->dev[d] + pl->dev_new[d]);
    177         if (pl->dev_new[d]) cat(buf, sizeof buf, "*");
    178     }
    179     if (buf[0]) gfx_text_fit(s, &font_small, x, y, w, buf, C_TEXT);
    180 }
    181 
    182 /* ------------------------------------------------------------ modales */
    183 
    184 static void modal_menu(Surface *s, const Ctrl *c, int bx, int by, int bw, int bh, HitList *hits)
    185 {
    186     int rowh = gfx_touch ? 26 : 14, pad = (rowh - 14) / 2, w = bw * 3 / 4 < 260 ? bw * 3 / 4 : 260;
    187     bool respond = c->intent == I_RESPOND;
    188     int h = 22 + c->nitems * rowh + 6 + (respond ? 26 : 0);
    189     int x = bx + (bw - w) / 2, y = by + (bh - h) / 2;
    190     if (c->intent == I_SETUP_OK) y = by + bh - h - 4;     /* abajo: que se vea lo que confirmas */
    191     box(s, x, y, w, h, C_PANEL, C_ACCENT);
    192     gfx_rect(s, x, y, w, 15, C_ACCENT);
    193     gfx_text_fit(s, &font_bold, x + 4, y + 1, w - (gfx_touch ? 30 : 8), tr(c->menu_title), C_OUTLINE);
    194     if (gfx_touch && ctrl_can_back(c) && c->intent != I_SETUP_OK) {   /* cerrar (Esc) con el dedo; el de confirmar ya tiene "Deshacer" */
    195         gfx_text(s, &font_bold, x + w - 14, y + 1, "x", C_OUTLINE);
    196         hit_add(hits, x + w - 26, y, 26, 15, PK_APP, 7);
    197     }
    198     int ly = y + 20;
    199     if (respond) {
    200         const char *p = ctrl_prompt(c);
    201         gfx_text_fit(s, &font_small, x + 4, ly, w - 8, p, C_TEXT);
    202         int len = gfx_text_w(&font_small, p), per = (w - 8) / font_small.w;
    203         if (len > w - 8) {
    204             const char *q = p;
    205             for (int i = 0; i < per && *q; i++) utf8_next(&q);
    206             gfx_text_fit(s, &font_small, x + 4, ly + 9, w - 8, q, C_TEXT);
    207         }
    208         ly += 26;
    209     }
    210     for (int i = 0; i < c->nitems; i++) {
    211         const MenuItem *it = &c->items[i];
    212         bool sel = i == c->icursor;
    213         if (sel) gfx_rect(s, x + 2, ly - 1, w - 4, rowh, C_ACCENT);
    214         else if (gfx_touch) gfx_frame(s, x + 2, ly - 1, w - 4, rowh, C_PANEL2);
    215         char k[2] = { it->key, 0 };
    216         Color tc = sel ? C_OUTLINE : it->enabled ? C_TEXT : C_DIM;
    217         gfx_text(s, &font_bold, x + 6, ly + pad, k, sel ? C_OUTLINE : C_ACCENT);
    218         gfx_text_fit(s, &font_normal, x + 20, ly + pad, w - 24, it->label, tc);
    219         hit_add(hits, x + 2, ly - 1, w - 4, rowh, PK_ITEM, i);
    220         ly += rowh;
    221     }
    222 }
    223 
    224 static void modal_cards(Surface *s, const Game *g, const Ctrl *c, int bx, int by, int bw, int bh, HitList *hits)
    225 {
    226     bool offer = c->intent == I_OFFER || c->intent == I_COUNTER;
    227     int cw = bw >= 400 ? 44 : 34, w = NRES * (cw + 4) + 60, rows = offer ? 2 : 1;
    228     /* tactil: los +/- y los botones mas altos */
    229     int ch = gfx_touch ? 54 : 40, pm = gfx_touch ? 26 : 14, bh2 = gfx_touch ? 26 : 15;
    230     int h = 42 + rows * ch + 44 + (bh2 - 15);
    231     int x = bx + (bw - w) / 2, y = by + (bh - h) / 2;
    232     box(s, x, y, w, h, C_PANEL, C_ACCENT);
    233     gfx_rect(s, x, y, w, 15, C_ACCENT);
    234     gfx_text_fit(s, &font_bold, x + 4, y + 1, w - 8, ctrl_prompt(c), C_OUTLINE);
    235     int x0 = x + 54;
    236     for (int r = 0; r < NRES; r++) {
    237         gfx_rect(s, x0 + r * (cw + 4), y + 20, cw, 12, TERRAIN_COLOR[r]);
    238         center_text(s, &font_small, x0 + r * (cw + 4), y + 22, cw, res_short(r), C_OUTLINE);
    239     }
    240     for (int row = 0; row < rows; row++) {
    241         int ly = y + 38 + row * ch;
    242         const uint8_t *sel = row ? c->sel2 : c->sel;
    243         if (offer) gfx_text(s, row == c->row ? &font_bold : &font_normal, x + 6, ly + 12, row ? tr(S_GET) : tr(S_GIVE), row == c->row ? C_ACCENT : C_TEXT);
    244         for (int r = 0; r < NRES; r++) {
    245             int cx = x0 + r * (cw + 4);
    246             bool cur = row == c->row && r == c->rescur;
    247             box(s, cx, ly, cw, ch - 4, cur ? C_PANEL2 : C_PANEL, cur ? C_CURSOR : C_DIM);
    248             char n[4] = { 0 };
    249             str_int(n, sizeof n, 0, sel[r]);
    250             center_text(s, &font_bold, cx, ly + 3, cw, n, sel[r] ? C_WHITE : C_DIM);
    251             int hw = cw / 2, ty = ly + 21 + (pm - 12) / 2;
    252             gfx_rect(s, cx + 1, ly + 21, hw - 1, pm, C_PANEL2);
    253             gfx_rect(s, cx + hw + 1, ly + 21, hw - 2, pm, C_PANEL2);
    254             center_text(s, &font_bold, cx + 1, ty, hw - 1, "-", C_TEXT);
    255             center_text(s, &font_bold, cx + hw, ty, hw, "+", C_TEXT);
    256             hit_add(hits, cx, ly + 20, hw, pm + 2, PK_CARD, row * NRES * 2 + r * 2 + 1);
    257             hit_add(hits, cx + hw, ly + 20, cw - hw, pm + 2, PK_CARD, row * NRES * 2 + r * 2);
    258         }
    259     }
    260     int ly = y + 38 + rows * ch;
    261     gfx_text(s, &font_small, x + 6, ly + 2, (c->intent == I_PLENTY || c->intent == I_GOLD) ? tr(S_BANK) : (g_lang ? "Have" : "Tenés"), C_DIM);
    262     for (int r = 0; r < NRES; r++) {
    263         char n[4] = { 0 };
    264         str_int(n, sizeof n, 0, (c->intent == I_PLENTY || c->intent == I_GOLD) ? g->bank[r] : g->p[c->actor].res[r]);
    265         center_text(s, &font_small, x0 + r * (cw + 4), ly + 2, cw, n, C_DIM);
    266     }
    267     int by2 = y + h - bh2 - 5, bt = by2 + (bh2 - 13) / 2;
    268     gfx_rect(s, x + w - 130, by2, 56, bh2, C_OK);
    269     center_text(s, &font_bold, x + w - 130, bt + 1, 56, "OK", C_OUTLINE);
    270     hit_add(hits, x + w - 130, by2, 56, bh2, PK_CARD, 100);
    271     gfx_rect(s, x + w - 68, by2, 62, bh2, C_DIM);
    272     center_text(s, &font_normal, x + w - 68, bt + 1, 62, g_lang ? "Cancel" : "Cancelar", C_OUTLINE);
    273     hit_add(hits, x + w - 68, by2, 62, bh2, PK_CARD, 101);
    274     gfx_text_fit(s, &font_small, x + 6, by2 + 4, w - 142, offer && !gfx_touch ? "Tab: das/pedís" : "", C_DIM);
    275 }
    276 
    277 static void modal_text(Surface *s, int bx, int by, int bw, int bh, const char *l1, const char *l2, Color accent)
    278 {
    279     int w = bw * 4 / 5, h = 50;
    280     int x = bx + (bw - w) / 2, y = by + (bh - h) / 2;
    281     box(s, x, y, w, h, C_PANEL, accent);
    282     center_text(s, &font_bold, x + 4, y + 12, w - 8, l1, accent);
    283     if (l2) center_text(s, &font_normal, x + 4, y + 30, w - 8, l2, C_TEXT);
    284 }
    285 
    286 static void modal_over(Surface *s, const Game *g, const UiInfo *ui, int bx, int by, int bw, int bh)
    287 {
    288     int w = bw * 4 / 5, h = 40 + g->o.np * 16 + (ui->can_free ? 16 : 0) + (ui->can_again ? 16 : 0);
    289     int x = bx + (bw - w) / 2, y = by + (bh - h) / 2;
    290     box(s, x, y, w, h, C_PANEL, C_ACCENT);
    291     char buf[64] = "";
    292     if (g->winner >= 0) { cat(buf, sizeof buf, ui->names[g->winner]); cat(buf, sizeof buf, " "); cat(buf, sizeof buf, tr(S_WINS)); }
    293     else cat(buf, sizeof buf, tr(S_P_OVER));
    294     center_text(s, &font_bold, x + 4, y + 10, w - 8, buf, C_ACCENT);
    295     int ly = y + 30;
    296     for (int p = 0; p < g->o.np; p++) {
    297         if (g->p[p].neutral) continue;
    298         gfx_rect(s, x + 12, ly + 1, 9, 9, PLAYER_COLOR[p]);
    299         gfx_text_fit(s, p == g->winner ? &font_bold : &font_normal, x + 26, ly - 1, w - 80, ui->names[p], C_TEXT);
    300         char v[16] = "";
    301         cati(v, sizeof v, game_vp(g, p)); cat(v, sizeof v, " "); cat(v, sizeof v, tr(S_VP));
    302         gfx_text(s, &font_bold, x + w - 60, ly - 1, v, C_ACCENT);
    303         ly += 16;
    304     }
    305     if (ui->can_free) { center_text(s, &font_normal, x + 4, ly + 2, w - 8, tr(S_GO_FREE), C_OK); ly += 16; }
    306     if (ui->can_again) center_text(s, &font_normal, x + 4, ly + 2, w - 8, tr(S_AGAIN), C_ACCENT);
    307 }
    308 
    309 static void modal_help(Surface *s, int bx, int by, int bw, int bh)
    310 {
    311     static const char *ES[] = { "Enter/Espacio: confirmar", "Flechas/hjkl: mover cursor", "Esc: volver", "r: dados   e: terminar",
    312                                 "1 ruta 2 pueblo 3 ciudad", "4: comprar carta  d: jugar", "b: banco   t: trueque   u: deshacer",
    313                                 "o: contraoferta (al responder)", "F2/click: chat  /w nombre: privado", "RePág/AvPág/rueda: registro",
    314                                 "F5: exportar la partida (texto)",
    315                                 "Tab: siguiente / detalle", "L: idioma   q: salir", "Mouse: click en todo" };
    316     static const char *EN[] = { "Enter/Space: confirm", "Arrows/hjkl: move cursor", "Esc: back", "r: roll   e: end turn",
    317                                 "1 road 2 settle 3 city", "4: buy card  d: play", "b: bank   t: trade   u: undo",
    318                                 "o: counter-offer (when answering)", "F2/click: chat  /w name: private", "PgUp/PgDn/wheel: log",
    319                                 "F5: export the game (text)",
    320                                 "Tab: next / details", "L: language   q: quit", "Mouse: click anything" };
    321     static const char *TES[] = { "Tocá un vértice, arista o hexágono", "para elegir dónde construir.", "Acciones: la lista bajo el tablero.",
    322                                  "Volver: cancela lo que estabas haciendo.", "Chat: tocá la caja de abajo.", "El registro se mueve con la barrita.",
    323                                  "Idioma y colores: en el menú principal.", "Salir: la partida queda guardada." };
    324     static const char *TEN[] = { "Tap a vertex, edge or hex", "to choose where to build.", "Actions: the list under the board.",
    325                                  "Back: cancels what you were doing.", "Chat: tap the box at the bottom.", "The log scrolls with the little bar.",
    326                                  "Language and colors: in the main menu.", "Quit: the game stays saved." };
    327     const char **L = g_lang ? EN : ES;
    328     int n = 14;
    329     if (gfx_touch) { L = g_lang ? TEN : TES; n = 8; }
    330     bool cards = bh >= 26 + (n + 1 + NDEV) * 12;          /* que hace cada carta, si hay lugar (la Pico no) */
    331     int w = bw * 4 / 5 < 260 ? bw * 4 / 5 : 260, h = 26 + (n + (cards ? 1 + NDEV : 0)) * 12;
    332     int x = bx + (bw - w) / 2, y = by + (bh - h) / 2;
    333     box(s, x, y, w, h, C_PANEL, C_ACCENT);
    334     gfx_text(s, &font_bold, x + 6, y + 4, g_lang ? "Help" : "Ayuda", C_ACCENT);
    335     for (int i = 0; i < n; i++) gfx_text_fit(s, &font_normal, x + 8, y + 22 + i * 12, w - 12, L[i], C_TEXT);
    336     if (cards) {
    337         int ly = y + 22 + n * 12;
    338         gfx_text(s, &font_bold, x + 8, ly, tr(S_DEV_CARDS), C_ACCENT);
    339         for (int d = 0; d < NDEV; d++) {
    340             char t[96] = "";
    341             cat(t, sizeof t, dev_name(d)); cat(t, sizeof t, ": "); cat(t, sizeof t, tr(S_DD_KNIGHT + d));
    342             gfx_text_fit(s, &font_normal, x + 8, ly + 12 * (d + 1), w - 12, t, C_TEXT);
    343         }
    344     }
    345 }
    346 
    347 static void modals(Surface *s, const Game *g, const Ctrl *c, const UiInfo *ui, int bx, int by, int bw, int bh, HitList *hits)
    348 {
    349     if (c->mode == CM_MENU) modal_menu(s, c, bx, by, bw, bh, hits);
    350     else if (c->mode == CM_CARDS) modal_cards(s, g, c, bx, by, bw, bh, hits);
    351     else if (c->mode == CM_HANDOFF) {
    352         char l1[64] = "";
    353         cat(l1, sizeof l1, tr(S_P_HANDOFF)); cat(l1, sizeof l1, " "); cat(l1, sizeof l1, ui->names[c->actor]);
    354         modal_text(s, bx, by, bw, bh, l1, tr(S_P_ENTER), PLAYER_COLOR[c->actor]);
    355         hit_add(hits, bx, by, bw, bh, PK_ITEM, 0);
    356     } else if (c->mode == CM_OVER || g->phase == PH_OVER) modal_over(s, g, ui, bx, by, bw, bh);
    357     if (c->mode == CM_OVER || g->phase == PH_OVER) {        /* tocar el cuadro: volver (9); la ultima linea: juego libre (10) */
    358         int w = bw * 4 / 5, h = 40 + g->o.np * 16 + (ui->can_free ? 16 : 0) + (ui->can_again ? 16 : 0);
    359         int x = bx + (bw - w) / 2, y = by + (bh - h) / 2, ly = y + h - 2;
    360         hit_add(hits, x, y, w, h, PK_APP, 9);
    361         if (ui->can_again) { hit_add(hits, x, ly - 16, w, 16, PK_APP, 14); ly -= 16; }
    362         if (ui->can_free) hit_add(hits, x, ly - 16, w, 16, PK_APP, 10);
    363     }
    364     if (ui->show_help) modal_help(s, bx, by, bw, bh);
    365     if (ui->modal == 2) {                                  /* menu de la app (tactil): ayuda, salir */
    366         static const int ID[3] = { 1, 0, 13 };
    367         const char *L[3] = { g_lang ? "Help" : "Ayuda", g_lang ? "Quit the game" : "Salir de la partida", g_lang ? "Close" : "Cerrar" };
    368         int w = 200, h = 22 + 3 * 28 + 6, x = bx + (bw - w) / 2, y = by + (bh - h) / 2;
    369         box(s, x, y, w, h, C_PANEL, C_ACCENT);
    370         gfx_rect(s, x, y, w, 15, C_ACCENT);
    371         gfx_text(s, &font_bold, x + 4, y + 1, "Catan", C_OUTLINE);
    372         for (int i = 0; i < 3; i++) {
    373             int ly = y + 20 + i * 28;
    374             box(s, x + 4, ly, w - 8, 26, C_PANEL2, C_DIM);
    375             center_text(s, &font_normal, x + 4, ly + 7, w - 8, L[i], C_TEXT);
    376             hit_add(hits, x + 4, ly, w - 8, 26, PK_APP, ID[i]);
    377         }
    378     }
    379     if (ui->modal == 1) {
    380         bool touch = ui->touch;
    381         modal_text(s, bx, by, bw, bh, ui->modal_text ? ui->modal_text : "?",
    382                    touch ? (g_lang ? "tap here: yes   outside: no" : "tocá acá: sí   afuera: no") : (g_lang ? "[y] yes   [n] no" : "[s] sí   [n] no"), C_ERR);
    383         int w = bw * 4 / 5, h = 50;
    384         hit_add(hits, bx + (bw - w) / 2, by + (bh - h) / 2, w, h, PK_APP, 8);
    385     }
    386 }
    387 
    388 
    389 
    390 /* vista previa del mapa: titulo y botones (ver UiInfo.preview) */
    391 static void preview_lines(Surface *s, const UiInfo *ui, int x, int y, int w, HitList *hits)
    392 {
    393     gfx_text_fit(s, &font_bold, x, y, w, ui->preview, C_ACCENT);
    394     const char *L[5] = { g_lang ? "< Map" : "< Mapa", g_lang ? "Reshuffle (R)" : "Otro reparto (R)", g_lang ? "Map >" : "Mapa >",
    395                          g_lang ? "Play (Enter)" : "Jugar (Enter)", g_lang ? "Back (Esc)" : "Volver (Esc)" };
    396     const char *S[5] = { "<", "R", ">", g_lang ? "Play" : "Jugar", g_lang ? "Back" : "Volver" };   /* si no entran */
    397     const Font *f = ui->touch ? &font_normal : &font_small;
    398     int bh = ui->touch ? 26 : 11, by = y + 13, total = 0;
    399     for (int i = 0; i < 5; i++) total += gfx_text_w(f, L[i]) + 8 + 3;
    400     const char **T = total <= w ? L : S;
    401     int bx = x;
    402     for (int i = 0; i < 5; i++) {
    403         int bw = gfx_text_w(f, T[i]) + 8;
    404         if (bx + bw > x + w) break;
    405         box(s, bx, by, bw, bh, i == 3 ? C_ACCENT : C_PANEL2, C_DIM);
    406         gfx_text(s, f, bx + 4, by + (bh - f->h) / 2 + 1, T[i], i == 3 ? C_OUTLINE : C_TEXT);
    407         hit_add(hits, bx, by, bw, bh, PK_APP, 20 + i);
    408         bx += bw + 3;
    409     }
    410 }
    411 
    412 static void bottom_lines(Surface *s, const Ctrl *c, const UiInfo *ui, int x, int y, int w, bool compact, HitList *hits)
    413 {
    414     if (ui->preview) { preview_lines(s, ui, x, y, w, hits); return; }
    415     if (ui->touch && ctrl_can_back(c)) {
    416         /* tactil: "Volver" (Esc) solo cuando hay algo que cancelar, a la derecha de la instruccion */
    417         int bw = 66;
    418         box(s, x + w - bw, y - 2, bw, 26, C_PANEL2, C_DIM);
    419         center_text(s, &font_bold, x + w - bw, y + 5, bw, g_lang ? "Back" : "Volver", C_TEXT);
    420         hit_add(hits, x + w - bw, y - 2, bw, 26, PK_APP, 7);
    421         w -= bw + 6;
    422     }
    423     gfx_text_fit(s, &font_bold, x, y, w, ctrl_prompt(c), C_ACCENT);
    424     const Font *f = compact ? &font_small : &font_normal;
    425     if (c->msg[0]) gfx_text_fit(s, f, x, y + 13, w, c->msg, c->msg_err ? C_ERR : C_OK);
    426     else if (ui->touch) gfx_text_fit(s, f, x, y + 13, w, g_lang ? "Tap the board, the actions or the cards" : "Tocá el tablero, las acciones o las cartas", C_DIM);
    427     else gfx_text_fit(s, f, x, y + 13, w, g_lang ? "? help · F2 chat · L language · q quit · Tab details" : "? ayuda · F2 chat · L idioma · q salir · Tab detalle", C_DIM);
    428 }
    429 
    430 /* Ultimas lineas del registro en "rows" filas de font_small; las largas siguen en
    431  * la fila de abajo con sangria (se cuentan desde el final hacia arriba). */
    432 static int wrap_at(const char *t, int w)
    433 {
    434     const char *p = t, *brk = 0;
    435     int px = 0;
    436     while (*p) {
    437         const char *q = p;
    438         utf8_next(&q);
    439         if (*p == ' ') brk = p;
    440         px += font_small.w;
    441         if (px > w) return (int)((brk && brk > t ? brk : p) - t);
    442         p = q;
    443     }
    444     return (int)(p - t);
    445 }
    446 
    447 static int line_rows(const char *t, int w)
    448 {
    449     if (*t >= '\1' && *t <= '\4' && t[1]) t += 2;
    450     int n = 1, k = wrap_at(t, w);
    451     while (t[k]) {
    452         t += k;
    453         while (*t == ' ') t++;
    454         k = wrap_at(t, w - 8);
    455         n++;
    456     }
    457     return n;
    458 }
    459 
    460 #define C_CHAT    RGB(130, 205, 255)
    461 #define C_WHISPER RGB(225, 150, 255)
    462 
    463 /* Color de una linea del registro (saltea la marca y el asiento): el chat va entero del
    464  * color de quien habla, lo que te paso a vos resaltado, y en las movidas solo el nombre
    465  * (*seat_name = asiento cuyo nombre se pinta, -1 ninguno). */
    466 static Color log_color(const char **t, bool last, int *seat_name)
    467 {
    468     *seat_name = -1;
    469     if (**t >= '\1' && **t <= '\4' && (*t)[1]) {
    470         char kind = **t;
    471         int seat = (*t)[1] - '0';
    472         bool ok = seat >= 0 && seat < MAXP;
    473         *t += 2;
    474         if (kind == '\4') return C_ACCENT;
    475         if (kind == '\3') { *seat_name = ok ? seat : -1; return last ? C_TEXT : C_DIM; }
    476         if (ok) return PLAYER_COLOR[seat];
    477         return kind == '\2' ? C_WHISPER : C_CHAT;
    478     }
    479     return last ? C_TEXT : C_DIM;
    480 }
    481 
    482 /* "rows" filas del registro terminando ui->log_scroll filas antes del final.
    483  * Devuelve el total de filas (para la barra de scroll). */
    484 static int log_lines(Surface *s, const UiInfo *ui, int x, int y, int w, int rows, int step)
    485 {
    486     int total = 0;
    487     for (int i = 0; i < ui->nlog; i++) total += line_rows(ui->log[i], w);
    488     int max = total > rows ? total - rows : 0, scroll = ui->log_scroll;
    489     if (ui->log_max) *ui->log_max = max;
    490     if (scroll > max) scroll = max;
    491     if (scroll < 0) scroll = 0;
    492     int first = total - scroll - rows, r = 0;
    493     if (first < 0) first = 0;
    494     for (int i = 0; i < ui->nlog && r < first + rows; i++) {
    495         const char *t = ui->log[i];
    496         int ns;
    497         Color c = log_color(&t, i == ui->nlog - 1, &ns);
    498         const char *nm = ns >= 0 ? ui->names[ns] : 0;
    499         size_t nl = nm ? strlen(nm) : 0;
    500         bool name_first = nl && !strncmp(t, nm, nl);
    501         int k = wrap_at(t, w), ind = 0;
    502         for (;;) {
    503             if (r >= first && r < first + rows) {
    504                 char part[256];
    505                 int n = k < (int)sizeof part - 1 ? k : (int)sizeof part - 1;
    506                 memcpy(part, t, (size_t)n);
    507                 part[n] = 0;
    508                 int yy = y + (r - first) * step, dx = 0;
    509                 if (name_first && ind == 0 && (size_t)n >= nl) {      /* el nombre de su color */
    510                     char who[64];
    511                     size_t wl = nl < sizeof who - 1 ? nl : sizeof who - 1;
    512                     memcpy(who, part, wl);
    513                     who[wl] = 0;
    514                     dx = gfx_text(s, &font_small, x, yy, who, PLAYER_COLOR[ns]);
    515                     gfx_text(s, &font_small, x + dx, yy, part + nl, c);
    516                 } else gfx_text(s, &font_small, x + ind, yy, part, c);
    517             }
    518             r++;
    519             t += k;
    520             while (*t == ' ') t++;
    521             if (!*t) break;
    522             ind = 8;
    523             k = wrap_at(t, w - ind);
    524         }
    525     }
    526     return total;
    527 }
    528 
    529 /* lo que se esta escribiendo en el chat, en lugar de la instruccion */
    530 /* Caja de chat siempre visible al pie del panel; con foco se escribe ahi. */
    531 static void chat_box(Surface *s, const UiInfo *ui, int x, int y, int w, int h, HitList *hits)
    532 {
    533     bool on = ui->chat_focus;
    534     gfx_rect(s, x, y, w, h, on ? C_PANEL2 : RGB(40, 44, 56));
    535     gfx_frame(s, x, y, w, h, on ? C_CHAT : RGB(70, 76, 92));
    536     hit_add(hits, x, y, w, h, PK_APP, 6);
    537     int ty = y + (h - 12) / 2 + 1;
    538     const char *text = ui->chat_input ? ui->chat_input : "";
    539     if (!on && !text[0]) {
    540         gfx_text_fit(s, &font_normal, x + 4, ty, w - 8,
    541                      g_lang ? "Type here to chat (click or F2)" : "Escribí acá para chatear (click o F2)", C_DIM);
    542         return;
    543     }
    544     int max = (w - 14) / font_normal.w, len = 0;
    545     for (const char *p = text; *p; ) { utf8_next(&p); len++; }
    546     const char *shown = text;
    547     while (len > max) { utf8_next(&shown); len--; }      /* si no entra, se ve el final */
    548     int tw = gfx_text(s, &font_normal, x + 4, ty, shown, C_WHITE);
    549     if (on) gfx_rect(s, x + 4 + tw, ty, 6, 11, C_CHAT);
    550 }
    551 
    552 /* Barra de scroll del registro: la barrita marca que parte se ve; click arriba o
    553  * abajo de ella pasa de pagina (PK_APP 4 / 5). */
    554 static void log_scrollbar(Surface *s, const UiInfo *ui, int x, int y, int h, int rows, int total, HitList *hits)
    555 {
    556     if (h <= 0 || rows <= 0) return;
    557     gfx_rect(s, x, y, 4, h, RGB(62, 68, 86));
    558     if (total <= rows) return;
    559     int max = total - rows, sc = ui->log_scroll > max ? max : ui->log_scroll;
    560     int th = h * rows / total;
    561     if (th < 8) th = 8;
    562     int ty = y + (h - th) * (max - sc) / max;
    563     gfx_rect(s, x, ty, 4, th, sc ? C_ACCENT : C_DIM);
    564     hit_add(hits, x - 3, y, 8, ty - y, PK_APP, 4);
    565     hit_add(hits, x - 3, ty + th, 8, y + h - ty - th, PK_APP, 5);
    566 }
    567 
    568 /* ------------------------------------------------------------ layout ancho */
    569 
    570 static void screen_wide(Surface *s, const Game *g, const Ctrl *c, const UiInfo *ui, HitList *hits, BoardGeom *geo)
    571 {
    572     /* el panel crece con pantallas anchas (16:9) para que entren los textos */
    573     int W = s->w, H = s->h, PW = W * 2 / 5, top = 16, bot = ui->preview && ui->touch ? 44 : 30;   /* la vista previa tactil lleva botones altos */
    574     if (PW < 230) PW = 230;
    575     if (PW > 340) PW = 340;
    576     int BW = W - PW;
    577     gfx_rect(s, 0, 0, W, H, C_PANEL);
    578     top_bar(s, g, ui, W, top, false, hits);
    579     gfx_rect(s, 0, top, BW, H - top - bot, C_SEA);
    580     geom_fit(geo, g, 0, top, BW, H - top - bot);
    581     gfx_clip(s, 0, top, BW, H - top - bot);
    582     gfx_board(s, geo, g, c, GB_ALL);
    583     gfx_noclip(s);
    584     hit_add(hits, 0, top, BW, H - top - bot, PK_BOARD, 0);
    585     bottom_lines(s, c, ui, 4, H - bot + 2, BW - 8, false, hits);
    586 
    587     int x = BW + 4, w = PW - 8, y = top + 4;
    588     gfx_text(s, &font_bold, x, y, tr(S_PLAYERS), C_ACCENT);
    589     y += 15;
    590     int rowh = g->o.np > 4 ? 21 : 27;
    591     for (int p = 0; p < g->o.np; p++) {
    592         bool det = !g->p[p].neutral;
    593         player_line(s, g, ui, p, x, y, w, det, rowh);
    594         y += det ? rowh : 14;
    595     }
    596     int vs = ui->view_seat;
    597     if (vs >= 0 && c->mode != CM_HANDOFF && !g->p[vs].neutral) {
    598         y += 3;
    599         char t[48] = "";
    600         cat(t, sizeof t, tr(S_HAND));
    601         gfx_text(s, &font_bold, x, y, t, C_ACCENT);
    602         y += 14;
    603         int cw = (w - 4 * 3) / NRES;
    604         for (int r = 0; r < NRES; r++) res_card(s, x + r * (cw + 3), y, cw, 30, r, g->p[vs].res[r], hits);
    605         y += 33;
    606         dev_list(s, g, vs, x, y, w);
    607         y += 10;
    608     }
    609     y += 4;
    610     gfx_text(s, &font_bold, x, y, tr(S_ACTIONS), C_ACCENT);
    611     y += 14;
    612     int colw = (w + 2) / 2;                  /* acciones y costos en dos columnas */
    613     int box = ui->chat_input ? (ui->touch ? 24 : 16) : 0;
    614     if (c->nactions && c->mode == CM_IDLE) {
    615         /* filas altas para el dedo, pero que dejen lugar a 3 filas de registro y el chat */
    616         int rows = (c->nactions + 1) / 2, ah = ui->touch ? 22 : 13;
    617         int avail = (H - 4 - box - 6 - 14 - 3 * 11 - y) / rows;
    618         if (ah > avail) ah = avail < 13 ? 13 : avail;
    619         for (int i = 0; i < c->nactions; i++) {
    620             const MenuItem *it = &c->actions[i];
    621             int ax = x - 2 + (i / rows) * colw, ay = y + (i % rows) * ah, ty = ay + (ah - 13) / 2;   /* por columnas */
    622             bool sel = i == c->icursor;
    623             if (sel) gfx_rect(s, ax, ay - 1, colw - 2, ah, C_ACCENT);
    624             else if (ui->touch) gfx_frame(s, ax, ay - 1, colw - 2, ah, C_PANEL2);
    625             char k[2] = { it->key, 0 };
    626             gfx_text(s, &font_bold, ax + 4, ty, k, sel ? C_OUTLINE : C_ACCENT);
    627             const uint8_t *cost = action_cost(it->action);      /* lo que cuesta, a la derecha: se ve cuando hace falta */
    628             int sq = colw >= 150 ? 8 : 6;                       /* en columnas angostas, mas chicos */
    629             int cw2 = cost ? cost_squares(s, ax + colw - 6, ay + (ah - sq - 3) / 2, cost, sq) + 4 : 0;
    630             gfx_text_fit(s, &font_normal, ax + 16, ty, colw - 20 - cw2, it->label, sel ? C_OUTLINE : it->enabled ? C_TEXT : C_DIM);
    631             hit_add(hits, ax, ay - 1, colw - 2, ah, PK_ACTION, i);
    632         }
    633         y += rows * ah;
    634     } else {
    635         static const struct { int name; const uint8_t *cost; } COSTS[] = {
    636             { S_ROAD, COST_ROAD }, { S_SETTLE, COST_SETTLE }, { S_CITY, COST_CITY }, { S_DEV_SHORT, COST_DEV } };
    637         for (int i = 0; i < 4; i++) {
    638             int cx0 = x + (i % 2) * colw, cy = y + (i / 2) * 13, cx = cx0 + colw - 5 * 12 - 4;   /* la ciudad lleva 5 */
    639             gfx_text_fit(s, &font_normal, cx0 + 2, cy, cx - cx0 - 4, tr(COSTS[i].name), C_DIM);
    640             for (int r = 0; r < NRES; r++)
    641                 for (int k = 0; k < COSTS[i].cost[r]; k++) { gfx_rect(s, cx, cy, 10, 11, TERRAIN_COLOR[r]); gfx_frame(s, cx, cy, 10, 11, C_OUTLINE); cx += 12; }
    642         }
    643         y += 26;
    644     }
    645     y += 6;
    646     gfx_text(s, &font_bold, x, y, tr(S_LOG), C_ACCENT);
    647     if (ui->log_scroll > 0) {
    648         const char *t = g_lang ? "history · End: latest" : "historial · Fin: lo último";
    649         gfx_text(s, &font_small, x + w - gfx_text_w(&font_small, t), y + 3, t, C_ACCENT);
    650     }
    651     hit_add(hits, x, y, w, H - y, PK_APP, 3);       /* rueda del mouse sobre el registro */
    652     y += 14;
    653     int rows = (H - 4 - box - y) / 11;
    654     if (rows < 1) rows = 1;
    655     int total = log_lines(s, ui, x, y, w - 7, rows, 11);
    656     log_scrollbar(s, ui, x + w - 4, y, rows * 11, rows, total, hits);
    657     if (ui->chat_input) chat_box(s, ui, x - 2, H - box - 2, w + 4, box, hits);
    658     modals(s, g, c, ui, 0, top, BW, H - top - bot, hits);
    659 }
    660 
    661 /* ------------------------------------------------------------ layout compacto */
    662 
    663 static void screen_info(Surface *s, const Game *g, const UiInfo *ui, int y0)
    664 {
    665     int W = s->w, H = s->h;
    666     gfx_rect(s, 0, y0, W, H - y0, C_PANEL);
    667     int y = y0 + 4;
    668     for (int p = 0; p < g->o.np; p++) {
    669         bool det = !g->p[p].neutral;
    670         player_line(s, g, ui, p, 4, y, W - 8, det, 27);
    671         y += det ? 27 : 14;
    672     }
    673     int vs = ui->view_seat;
    674     if (vs >= 0 && !g->p[vs].neutral) { dev_list(s, g, vs, 6, y, W - 12); y += 12; }
    675     gfx_text(s, &font_bold, 4, y, tr(S_LOG), C_ACCENT);
    676     y += 13;
    677     int rows = (H - y - 2) / 9;
    678     int start = ui->nlog > rows ? ui->nlog - rows : 0;
    679     for (int i = start; i < ui->nlog; i++) {
    680         const char *t = ui->log[i];
    681         int ns;
    682         Color c = log_color(&t, i == ui->nlog - 1, &ns);
    683         gfx_text_fit(s, &font_small, 4, y, W - 8, t, c);
    684         y += 9;
    685     }
    686 }
    687 
    688 static void screen_compact(Surface *s, const Game *g, const Ctrl *c, const UiInfo *ui, HitList *hits, BoardGeom *geo)
    689 {
    690     int W = s->w, H = s->h, top = 11, bh = 238;
    691     gfx_rect(s, 0, 0, W, H, C_PANEL);
    692     top_bar(s, g, ui, W, top, true, hits);
    693     hit_add(hits, 0, 0, 60, top, PK_APP, 2);
    694     if (ui->show_info) {
    695         screen_info(s, g, ui, top);
    696         return;
    697     }
    698     gfx_rect(s, 0, top, W, bh, C_SEA);
    699     geom_fit(geo, g, 0, top, W, bh);
    700     gfx_clip(s, 0, top, W, bh);
    701     gfx_board(s, geo, g, c, GB_ALL);
    702     gfx_noclip(s);
    703     hit_add(hits, 0, top, W, bh, PK_BOARD, 0);
    704 
    705     int y = top + bh + 2;
    706     /* jugadores */
    707     int pw = W / 4;
    708     for (int p = 0, k = 0; p < g->o.np; p++) {
    709         const Player *pl = &g->p[p];
    710         int x = k++ * pw;
    711         if (p == g->cur) gfx_rect(s, x, y - 1, pw - 2, 10, C_PANEL2);
    712         gfx_rect(s, x + 2, y, 7, 7, PLAYER_COLOR[p]);
    713         char buf[24] = "";
    714         if (pl->neutral) cat(buf, sizeof buf, "N");
    715         else {
    716             cati(buf, sizeof buf, game_vp_public(g, p)); cat(buf, sizeof buf, g_lang ? "vp " : "pv ");
    717             cati(buf, sizeof buf, hand_size(g, p)); cat(buf, sizeof buf, "c");
    718         }
    719         gfx_text_fit(s, &font_small, x + 11, y, pw - 14, buf, pl->neutral ? C_DIM : C_TEXT);
    720     }
    721     y += 11;
    722     /* mano */
    723     int vs = ui->view_seat;
    724     if (vs >= 0 && c->mode != CM_HANDOFF && !g->p[vs].neutral) {
    725         int cw = (W - 4 - 4 * 2) / NRES;
    726         for (int r = 0; r < NRES; r++) {
    727             int x = 2 + r * (cw + 2), n = g->p[vs].res[r];
    728             gfx_rect(s, x, y, cw, 11, n ? TERRAIN_COLOR[r] : dim(TERRAIN_COLOR[r]));
    729             char buf[16] = "";
    730             cat(buf, sizeof buf, res_short(r)); cat(buf, sizeof buf, " "); cati(buf, sizeof buf, n);
    731             center_text(s, &font_small, x, y + 2, cw, buf, C_OUTLINE);
    732         }
    733     }
    734     y += 13;
    735     /* acciones: una fila de atajos */
    736     if (c->nactions && c->mode == CM_IDLE) {
    737         int x = 2;
    738         for (int i = 0; i < c->nactions; i++) {
    739             const MenuItem *it = &c->actions[i];
    740             char buf[20] = { it->key, ':', 0 };
    741             const char *l = it->label;
    742             int n = 2;
    743             while (*l && *l != ' ' && n < 9) buf[n++] = *l++;
    744             buf[n] = 0;
    745             int tw = gfx_text_w(&font_small, buf) + 4;
    746             if (x + tw > W) { x = 2; y += 10; }
    747             bool sel = i == c->icursor;
    748             if (sel) gfx_rect(s, x - 1, y - 1, tw, 9, C_ACCENT);
    749             gfx_text(s, &font_small, x + 1, y, buf, sel ? C_OUTLINE : it->enabled ? C_TEXT : C_DIM);
    750             hit_add(hits, x - 1, y - 1, tw, 10, PK_ACTION, i);
    751             x += tw + 2;
    752         }
    753         y += 11;
    754     }
    755     if (y > H - 25) y = H - 25;
    756     bottom_lines(s, c, ui, 2, y, W - 4, true, hits);
    757     modals(s, g, c, ui, 0, top, W, bh, hits);
    758 }
    759 
    760 /* ------------------------------------------------------------ layout vertical */
    761 
    762 /* Pantalla mas alta que ancha (el telefono en vertical): el tablero arriba, del ancho
    763  * entero, y abajo el panel del layout ancho apilado: jugadores, mano, acciones,
    764  * registro y chat. Las zonas clickeables llevan los mismos ids que en el ancho. */
    765 static void screen_tall(Surface *s, const Game *g, const Ctrl *c, const UiInfo *ui, HitList *hits, BoardGeom *geo)
    766 {
    767     int W = s->w, H = s->h, top = 16;
    768     int bh = W * 9 / 10;                     /* el tablero es un poco mas ancho que alto */
    769     if (bh > H / 2) bh = H / 2;
    770     /* lo que necesita el panel de abajo; si no entra, primero se achican las filas de
    771      * acciones y despues el tablero (hasta W/2), para que nada quede fuera de la pantalla
    772      * ni la caja de chat tape a "Terminar" */
    773     int rowh = 21, ah_max = ui->touch ? 22 : 14, rh = ui->touch ? 30 : 26;
    774     int box_h = ui->chat_input ? (ui->touch ? 26 : 18) : 0;
    775     int vs = ui->view_seat;
    776     bool hand = vs >= 0 && c->mode != CM_HANDOFF && !g->p[vs].neutral;
    777     int arows = c->nactions && c->mode == CM_IDLE ? (c->nactions + 1) / 2 : 0;
    778     int fixed = 3 + 30 + 2 + 4 + 14 + 3 * 11 + box_h + 4;      /* instruccion, titulo del registro, 3 filas, chat */
    779     for (int p = 0; p < g->o.np; p++) fixed += g->p[p].neutral ? 14 : rowh;
    780     if (hand) fixed += 2 + rh + 3 + 10;
    781     int avail = H - top - bh - fixed, ah = ah_max;
    782     if (arows) {
    783         ah = avail / arows;
    784         if (ah > ah_max) ah = ah_max;
    785         if (ah < 13) ah = 13;
    786         int need = arows * ah - avail;
    787         if (need > 0) { bh -= need; if (bh < W / 2) bh = W / 2; }
    788     }
    789     gfx_rect(s, 0, 0, W, H, C_PANEL);
    790     top_bar(s, g, ui, W, top, false, hits);
    791     gfx_rect(s, 0, top, W, bh, C_SEA);
    792     geom_fit(geo, g, 0, top, W, bh);
    793     gfx_clip(s, 0, top, W, bh);
    794     gfx_board(s, geo, g, c, GB_ALL);
    795     gfx_noclip(s);
    796     hit_add(hits, 0, top, W, bh, PK_BOARD, 0);
    797     int x = 4, w = W - 8, y = top + bh + 3;
    798     bottom_lines(s, c, ui, x, y, w, false, hits);
    799     y += ui->preview && ui->touch ? 44 : 30;     /* la vista previa tactil lleva botones altos */
    800 
    801     for (int p = 0; p < g->o.np; p++) {
    802         bool det = !g->p[p].neutral;
    803         player_line(s, g, ui, p, x, y, w, det, rowh);
    804         y += det ? rowh : 14;
    805     }
    806     if (hand) {
    807         y += 2;
    808         int cw = (w - 4 * 3) / NRES;
    809         for (int r = 0; r < NRES; r++) res_card(s, x + r * (cw + 3), y, cw, rh, r, g->p[vs].res[r], hits);
    810         y += rh + 3;
    811         dev_list(s, g, vs, x, y, w);
    812         y += 10;
    813     }
    814     y += 2;
    815     int colw = (w + 2) / 2;
    816     if (arows) {
    817         for (int i = 0; i < c->nactions; i++) {
    818             const MenuItem *it = &c->actions[i];
    819             int ax = x - 2 + (i / arows) * colw, ay = y + (i % arows) * ah, ty = ay + (ah - 14) / 2;
    820             bool sel = i == c->icursor;
    821             box(s, ax, ay - 1, colw - 2, ah, sel ? C_ACCENT : C_PANEL2, sel ? C_ACCENT : C_DIM);
    822             char k[2] = { it->key, 0 };
    823             gfx_text(s, &font_bold, ax + 4, ty, k, sel ? C_OUTLINE : C_ACCENT);
    824             const uint8_t *cost = action_cost(it->action);
    825             int sq = colw >= 150 ? 8 : 6;                       /* en columnas angostas, mas chicos */
    826             int cw2 = cost ? cost_squares(s, ax + colw - 6, ay + (ah - sq - 3) / 2, cost, sq) + 4 : 0;
    827             gfx_text_fit(s, &font_normal, ax + 16, ty, colw - 20 - cw2, it->label, sel ? C_OUTLINE : it->enabled ? C_TEXT : C_DIM);
    828             hit_add(hits, ax, ay - 1, colw - 2, ah, PK_ACTION, i);
    829         }
    830         y += arows * ah;
    831     }
    832     y += 4;
    833     gfx_text(s, &font_bold, x, y, tr(S_LOG), C_ACCENT);
    834     if (ui->log_scroll > 0) {
    835         const char *t = g_lang ? "history" : "historial";
    836         gfx_text(s, &font_small, x + w - gfx_text_w(&font_small, t), y + 3, t, C_ACCENT);
    837     }
    838     hit_add(hits, x, y, w, H - y, PK_APP, 3);
    839     y += 14;
    840     int chat_y = H - box_h - 2;
    841     if (chat_y < y + 11) chat_y = y + 11;    /* nunca encima de lo de arriba */
    842     int rows = (chat_y - 2 - y) / 11;
    843     if (rows < 1) rows = 1;
    844     int total = log_lines(s, ui, x, y, w - 7, rows, 11);
    845     log_scrollbar(s, ui, x + w - 4, y, rows * 11, rows, total, hits);
    846     if (ui->chat_input) chat_box(s, ui, x - 2, chat_y, w + 4, box_h, hits);
    847     modals(s, g, c, ui, 0, top, W, bh, hits);
    848 }
    849 
    850 void gfx_screen(Surface *s, const Game *g, const Ctrl *c, const UiInfo *ui, HitList *hits, BoardGeom *geo)
    851 {
    852     if (hits) hits->n = 0;
    853     if (ui->layout == LAYOUT_COMPACT) screen_compact(s, g, c, ui, hits, geo);
    854     else if (s->h > s->w) screen_tall(s, g, c, ui, hits, geo);
    855     else screen_wide(s, g, c, ui, hits, geo);
    856 }
    857 
    858 /* ------------------------------------------------------------ menus */
    859 
    860 /* fondo de mar y logo: cinco hexagonos con las letras */
    861 int catan_form_backdrop(Surface *s, bool compact)
    862 {
    863     int W = s->w, H = s->h;
    864     gfx_rect(s, 0, 0, W, H, C_SEA);
    865     BoardGeom g = { W / 2, compact ? 20 : 36, compact ? 12 : 20, 0 };
    866     static const char *L[] = { "C", "A", "T", "A", "N" };
    867     for (int i = 0; i < 5; i++) {
    868         int cx = g.cx + (i - 2) * g.R * 2 - (i - 2) * g.R / 4;
    869         int32_t xs[6], ys[6];
    870         static const int8_t CX[6] = { 0, 1, 1, 0, -1, -1 }, CY[6] = { -2, -1, 1, 2, 1, -1 };
    871         for (int k = 0; k < 6; k++) { xs[k] = cx * 16 + CX[k] * g.R * 887 / 64; ys[k] = g.cy * 16 + CY[k] * g.R * 8; }
    872         gfx_poly16(s, xs, ys, 6, C_SAND);
    873         for (int k = 0; k < 6; k++) { xs[k] = cx * 16 + CX[k] * g.R * 887 / 64 * 88 / 100; ys[k] = g.cy * 16 + CY[k] * g.R * 8 * 88 / 100; }
    874         gfx_poly16(s, xs, ys, 6, TERRAIN_COLOR[i]);
    875         gfx_text(s, &font_bold, cx - 2, g.cy - 6, L[i], C_OUTLINE);
    876     }
    877     return g.cy + g.R * 2 + (compact ? 4 : 12);
    878 }