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 }