tui.c (26833B)
1 /* tui.c - interfaz de terminal con ncurses. 2 * 3 * El tablero se dibuja con el renderer de pixeles (gfx) sobre un buffer donde 4 * cada celda de la terminal son 2 pixeles verticales (caracter '▀' con color de 5 * frente arriba y de fondo abajo). Encima se escriben como texto los numeros, 6 * edificios, ladron y puertos. Los paneles son texto comun. */ 7 #include "tuikit.h" 8 #include <stdlib.h> 9 #include <string.h> 10 #include "frontend.h" 11 12 /* ------------------------------------------------------------------ tablero */ 13 14 static void draw_board(Frontend *f, const Game *g, const Ctrl *c, int x0, int y0, int w, int h) 15 { 16 Surface s = *tk_pix_begin(w, h); 17 gfx_rect(&s, 0, 0, s.w, s.h, C_SEA); 18 BoardGeom geo; 19 geom_fit(&geo, g, 0, 0, s.w, s.h); 20 int flags = GB_ROADS | GB_PORTS | (geo.R >= 18 ? GB_DETAIL : 0); 21 gfx_board(&s, &geo, g, 0, flags); 22 23 const Topo *t = game_topo(g); 24 int cur = ctrl_cursor_pos(c); 25 Color ccol = ctrl_cursor_target(c) >= 0 ? C_CURSOR : C_DIM; 26 int br = geo.R / 4 > 2 ? geo.R / 4 : 2; 27 if (c->mode == CM_EDGE) { 28 for (int i = 0; i < c->ntargets; i++) { 29 int x, y; 30 if (c->targets[i] == cur) continue; 31 geom_edge(&geo, c->targets[i], &x, &y); 32 gfx_disc(&s, x, y, 2, C_OUTLINE); 33 gfx_disc(&s, x, y, 1, C_WHITE); 34 } 35 if (cur >= 0) { 36 int ax, ay, bx, by; 37 geom_vert(&geo, t->edge_v[cur][0], &ax, &ay); 38 geom_vert(&geo, t->edge_v[cur][1], &bx, &by); 39 gfx_line(&s, ax, ay, bx, by, geo.R / 4 > 3 ? geo.R / 4 : 3, ccol); 40 } 41 } 42 for (int v = 0; v < t->nvert; v++) { 43 if (g->vown[v] < 0) continue; 44 int x, y; 45 geom_vert(&geo, v, &x, &y); 46 int r = g->vlev[v] == 2 ? br + 1 : br; 47 gfx_disc(&s, x, y, r + 1, C_OUTLINE); 48 gfx_disc(&s, x, y, r, PLAYER_COLOR[g->vown[v]]); 49 } 50 { /* colocacion en borrador */ 51 int dv, de; 52 ctrl_draft(c, &dv, &de); 53 Color pc = c->actor >= 0 ? PLAYER_COLOR[c->actor] : C_WHITE; 54 if (de >= 0) { 55 int ax, ay, bx, by; 56 geom_vert(&geo, t->edge_v[de][0], &ax, &ay); 57 geom_vert(&geo, t->edge_v[de][1], &bx, &by); 58 gfx_line(&s, ax, ay, bx, by, geo.R / 5 > 2 ? geo.R / 5 : 2, pc); 59 } 60 if (dv >= 0) { 61 int x, y; 62 geom_vert(&geo, dv, &x, &y); 63 gfx_disc(&s, x, y, br + 1, C_OUTLINE); 64 gfx_disc(&s, x, y, br, pc); 65 } 66 } 67 if (c->mode == CM_VERT) { 68 for (int i = 0; i < c->ntargets; i++) { 69 int x, y; 70 if (c->targets[i] == cur) continue; 71 geom_vert(&geo, c->targets[i], &x, &y); 72 gfx_disc(&s, x, y, 2, C_OUTLINE); 73 gfx_disc(&s, x, y, 1, C_WHITE); 74 } 75 if (cur >= 0) { 76 int x, y; 77 geom_vert(&geo, cur, &x, &y); 78 gfx_disc(&s, x, y, br + 2, C_OUTLINE); 79 gfx_disc(&s, x, y, br + 1, ccol); 80 } 81 } 82 83 /* pixeles -> medios bloques */ 84 tk_pix_blit(y0, x0); 85 86 /* puertos */ 87 for (int i = 0; i < t->nport; i++) { 88 int px, py; 89 geom_port(&geo, i, &px, &py); 90 int k = g->port[i]; 91 Color bg = k == PORT_ANY ? C_WHITE : TERRAIN_COLOR[k]; 92 tk_pix_text(y0, x0, px - 1, py, k == PORT_ANY ? "3:1" : "2:1", C_BLACK, bg, A_BOLD); 93 if (k != PORT_ANY) { 94 char sh[8]; 95 snprintf(sh, sizeof sh, "%.3s", res_short(k)); 96 tk_pix_text(y0, x0, px - 1, py + 2, res_short(k), C_BLACK, bg, 0); 97 } 98 } 99 100 /* fichas numeradas */ 101 for (int hx = 0; hx < t->nhex; hx++) { 102 int x, y; 103 geom_hex(&geo, hx, &x, &y); 104 bool target = c->mode == CM_HEX && ctrl_is_target(c, hx); 105 bool iscur = c->mode == CM_HEX && cur == hx; 106 Color bg = iscur ? ccol : target ? C_WHITE : C_TOKEN; 107 char txt[8]; 108 if (g->num[hx]) snprintf(txt, sizeof txt, "%2d ", g->num[hx]); 109 else if (target || iscur) snprintf(txt, sizeof txt, " · "); 110 else continue; 111 Color fg = (g->num[hx] == 6 || g->num[hx] == 8) ? C_RED : C_BLACK; 112 tk_pix_text(y0, x0, x - 1, y, txt, fg, bg, A_BOLD); 113 if (geo.R >= 11 && g->num[hx]) { 114 static const int PIPS[13] = { 0, 0, 1, 2, 3, 4, 5, 0, 5, 4, 3, 2, 1 }; 115 char pips[16] = { 0 }; 116 int n = PIPS[g->num[hx]]; 117 for (int i = 0; i < n; i++) strcat(pips, "·"); 118 tk_pix_text(y0, x0, x - n / 2, y + 2, pips, fg, TERRAIN_COLOR[g->terrain[hx]], A_BOLD); 119 } 120 } 121 122 /* ladron */ 123 { 124 int x, y; 125 geom_hex(&geo, g->robber, &x, &y); 126 int rx = g->terrain[g->robber] == T_DESERT ? x : x - geo.R * 55 / 100 - 1; 127 tk_pix_text(y0, x0, rx, y, "♟", C_WHITE, RGB(30, 30, 34), A_BOLD); 128 } 129 130 /* edificios */ 131 for (int v = 0; v < t->nvert; v++) { 132 int x, y; 133 geom_vert(&geo, v, &x, &y); 134 if (g->vown[v] >= 0) 135 tk_pix_text(y0, x0, x, y, g->vlev[v] == 2 ? "♜" : "⌂", C_BLACK, PLAYER_COLOR[g->vown[v]], A_BOLD); 136 } 137 if (c->mode == CM_VERT && cur >= 0) { 138 int x, y; 139 geom_vert(&geo, cur, &x, &y); 140 tk_pix_text(y0, x0, x, y, "◆", C_BLACK, C_CURSOR, A_BOLD); 141 } 142 143 hit_add(&f->hits, x0, y0, w, h, PK_BOARD, 0); 144 f->geom = geo; 145 f->board_ox = x0; 146 f->board_oy = y0; 147 f->board_sy = 2; 148 } 149 150 /* ------------------------------------------------------------------ paneles */ 151 152 static void res_chip(int y, int x, int r, int n, bool dim) 153 { 154 char buf[16]; 155 snprintf(buf, sizeof buf, " %s %d ", res_short(r), n); 156 tk_attc(C_BLACK, TERRAIN_COLOR[r], dim ? 0 : A_BOLD); 157 mvaddstr(y, x, buf); 158 } 159 160 static void draw_panel(Frontend *f, const Game *g, const Ctrl *c, const UiInfo *ui, int x, int y, int w, int h) 161 { 162 int line = y, bottom = y + h; 163 tk_section(line++, x, w, tr(S_PLAYERS)); 164 for (int p = 0; p < g->o.np && line < bottom; p++) { 165 const Player *pl = &g->p[p]; 166 char buf[96]; 167 tk_att(pl->neutral ? 244 : TK_DEF, TK_DEF, 0); 168 mvaddstr(line, x, p == g->cur && g->phase != PH_OVER ? "▶" : " "); 169 tk_attc(PLAYER_COLOR[p], C_BLACK, 0); 170 mvaddstr(line, x + 1, "██"); 171 tk_att(pl->neutral ? 244 : TK_DEF, TK_DEF, p == ui->view_seat ? A_BOLD : 0); 172 tk_put(line, x + 4, ui->names[p], 14); 173 if (pl->neutral) { 174 snprintf(buf, sizeof buf, "%s · %s %d", tr(S_NEUTRAL), tr(S_ROAD), pl->road_len); 175 tk_att(244, TK_DEF, 0); 176 tk_put(line, x + 19, buf, w - 20); 177 line++; 178 continue; 179 } 180 int vp = game_vp_public(g, p); 181 int hidden = (p == ui->view_seat || g->phase == PH_OVER) ? pl->dev[D_VP] + pl->dev_new[D_VP] : 0; 182 if (hidden) snprintf(buf, sizeof buf, "%2d+%d %s", vp, hidden, tr(S_VP)); 183 else snprintf(buf, sizeof buf, "%2d %s", vp, tr(S_VP)); 184 tk_att(tk_xterm(C_ACCENT), TK_DEF, A_BOLD); 185 mvaddstr(line, x + 19, buf); 186 /* fichas que le quedan: pueblos, ciudades, rutas */ 187 if (game_has_sea(g)) snprintf(buf, sizeof buf, "⌂%d ♜%d ═%d ≈%d", pl->settles, pl->cities, pl->roads, pl->ships); 188 else snprintf(buf, sizeof buf, "⌂%d ♜%d ═%d", pl->settles, pl->cities, pl->roads); 189 if (x + 27 + tk_ulen(buf) <= x + w) { 190 tk_attc(PLAYER_COLOR[p], C_BLACK, 0); 191 tk_put(line, x + w - tk_ulen(buf) - 1, buf, w); 192 } 193 line++; 194 if (line >= bottom) break; 195 int devs = pl->dev_hidden; 196 for (int d = 0; d < NDEV; d++) devs += pl->dev[d] + pl->dev_new[d]; 197 int nc = hand_size(g, p); 198 snprintf(buf, sizeof buf, " %d %s · %d %s · %d %s · %s %d", nc, tr(nc == 1 ? S_CARD1 : S_CARDS), devs, g_lang ? "dev" : "des", 199 pl->knights, g_lang ? "kn" : "cab", g_lang ? "road" : "ruta", pl->road_len); 200 if (g->o.variant2p) snprintf(buf + strlen(buf), sizeof buf - strlen(buf), " · %d %s", pl->tokens, tr(S_TOKENS)); 201 tk_att(248, TK_DEF, 0); 202 tk_put(line, x, buf, w); 203 int bx = x + tk_ulen(buf) + 1; 204 if (g->longest == p && bx + 3 < x + w) { tk_att(tk_xterm(C_BLACK), tk_xterm(C_ACCENT), A_BOLD); mvaddstr(line, bx, "LR"); bx += 3; } 205 if (g->army == p && bx + 3 < x + w) { tk_att(tk_xterm(C_BLACK), tk_xterm(C_ERR), A_BOLD); mvaddstr(line, bx, "EJ"); } 206 line++; 207 } 208 209 /* mano */ 210 int vs = ui->view_seat; 211 if (line + 3 < bottom && vs >= 0 && c->mode != CM_HANDOFF && !g->p[vs].neutral) { 212 char title[64]; 213 snprintf(title, sizeof title, "%s — %s", tr(S_HAND), ui->names[vs]); 214 tk_section(line++, x, w, title); 215 const Player *pl = &g->p[vs]; 216 int cx = x + 1; 217 for (int r = 0; r < NRES; r++) { 218 res_chip(line, cx, r, pl->res[r], pl->res[r] == 0); 219 cx += 8; 220 } 221 line++; 222 char buf[128] = ""; 223 for (int d = 0; d < NDEV; d++) { 224 if (!pl->dev[d] && !pl->dev_new[d]) continue; 225 char one[48]; 226 snprintf(one, sizeof one, "%s%s×%d%s", buf[0] ? " · " : "", dev_name(d), pl->dev[d] + pl->dev_new[d], pl->dev_new[d] ? "*" : ""); 227 strncat(buf, one, sizeof buf - strlen(buf) - 1); 228 } 229 if (buf[0] && line < bottom) { tk_att(tk_xterm(C_TEXT), TK_DEF, 0); tk_put(line++, x + 1, buf, w - 2); } 230 } 231 232 /* acciones */ 233 if (line + 2 < bottom) { 234 tk_section(line++, x, w, tr(S_ACTIONS)); 235 if (c->nactions && c->mode == CM_IDLE) { 236 for (int i = 0; i < c->nactions && line < bottom; i++) { 237 const MenuItem *it = &c->actions[i]; 238 char buf[80]; 239 snprintf(buf, sizeof buf, " %c %s", it->key, it->label); 240 bool sel = i == c->icursor; 241 if (sel) tk_att(tk_xterm(C_BLACK), tk_xterm(C_ACCENT), A_BOLD); 242 else tk_att(it->enabled ? TK_DEF : 244, TK_DEF, 0); 243 tk_fill(line, x, w, " "); 244 tk_put(line, x, buf, w); 245 hit_add(&f->hits, x, line, w, 1, PK_ACTION, i); 246 line++; 247 } 248 } else { 249 static const struct { int name; const uint8_t *cost; } COSTS[] = { 250 { S_ROAD, COST_ROAD }, { S_SETTLE, COST_SETTLE }, { S_CITY, COST_CITY }, { S_DEV_SHORT, COST_DEV } }; 251 for (int i = 0; i < 4 && line < bottom; i++) { 252 tk_att(246, TK_DEF, 0); 253 tk_put(line, x + 1, tr(COSTS[i].name), 11); 254 int cx = x + 13; 255 for (int r = 0; r < NRES; r++) 256 for (int k = 0; k < COSTS[i].cost[r] && cx + 4 < x + w; k++) { 257 char chip[8]; 258 snprintf(chip, sizeof chip, "%.3s", res_short(r)); 259 tk_attc(C_BLACK, TERRAIN_COLOR[r], 0); 260 mvaddstr(line, cx, chip); 261 cx += 4; 262 } 263 line++; 264 } 265 } 266 } 267 268 /* registro */ 269 if (line + 2 < bottom) { 270 tk_section(line++, x, w, tr(S_LOG)); 271 if (ui->chat_input) { /* linea de chat siempre visible, abajo de todo */ 272 bottom--; 273 char cb[260]; 274 const char *t = ui->chat_input; 275 int room = w - 6, cl = tk_ulen(t); 276 while (cl > room) { utf8_next(&t); cl--; } 277 if (!ui->chat_focus && !*t) { 278 tk_att(244, 237, 0); 279 snprintf(cb, sizeof cb, "%s", g_lang ? "> type here to chat (F2)" : "> escribí acá para chatear (F2)"); 280 } else { 281 tk_att(117, 237, ui->chat_focus ? A_BOLD : 0); 282 snprintf(cb, sizeof cb, "> %s", t); 283 } 284 tk_fill(bottom, x, w, " "); 285 tk_put(bottom, x + 1, cb, w - 2); 286 if (ui->chat_focus) { tk_att(tk_xterm(C_BLACK), tk_xterm(C_ACCENT), 0); mvaddstr(bottom, x + 1 + tk_ulen(cb), " "); } 287 hit_add(&f->hits, x, bottom, w, 1, PK_APP, 6); 288 } 289 int rows = bottom - line; 290 int max = ui->nlog > rows ? ui->nlog - rows : 0, scroll = ui->log_scroll; 291 if (ui->log_max) *ui->log_max = max; 292 if (scroll > max) scroll = max; 293 int start = ui->nlog - rows - scroll; 294 if (start < 0) start = 0; 295 for (int i = start; i < ui->nlog && line < bottom; i++) { 296 const char *t = ui->log[i]; 297 int base = i == ui->nlog - 1 ? TK_DEF : 248, seat = -1; 298 char kind = *t >= '\1' && *t <= '\4' && t[1] ? *t : 0; 299 if (kind) { seat = t[1] - '0'; if (seat < 0 || seat >= MAXP) seat = -1; t += 2; } 300 if (kind == '\1' || kind == '\2') { /* chat: todo del color de quien habla */ 301 if (seat >= 0) tk_att(tk_xterm(PLAYER_COLOR[seat]), TK_DEF, A_BOLD); 302 else tk_att(kind == '\1' ? 117 : 183, TK_DEF, 0); 303 } else if (kind == '\4') tk_att(tk_xterm(C_ACCENT), TK_DEF, A_BOLD); /* te paso a vos */ 304 else tk_att(base, TK_DEF, 0); 305 size_t nl = kind == '\3' && seat >= 0 ? strlen(ui->names[seat]) : 0; 306 if (nl && !strncmp(t, ui->names[seat], nl)) { /* movida: el nombre de su color */ 307 char who[64]; 308 snprintf(who, sizeof who, "%.*s", (int)nl, t); 309 tk_att(tk_xterm(PLAYER_COLOR[seat]), TK_DEF, A_BOLD); 310 tk_put(line, x + 1, who, w - 2); 311 tk_att(base, TK_DEF, 0); 312 int wn = tk_ulen(who); 313 tk_put(line++, x + 1 + wn, t + nl, w - 2 - wn); 314 } else tk_put(line++, x + 1, t, w - 2); 315 } 316 if (scroll > 0) { 317 tk_att(tk_xterm(C_ACCENT), TK_DEF, 0); 318 tk_put(bottom - 1, x + w - 12, g_lang ? "▼ End" : "▼ Fin", 10); 319 } 320 } 321 } 322 323 /* ------------------------------------------------------------------ modales */ 324 325 static void draw_menu_modal(Frontend *f, const Ctrl *c, int bx, int by, int bw, int bh) 326 { 327 int w = 44, h = c->nitems + 4; 328 const char *title = tr(c->menu_title); 329 if (c->intent == I_RESPOND) h += 2; 330 int x = bx + (bw - w) / 2, y = by + (bh - h) / 2; 331 tk_box(y, x, h, w, title, C_ACCENT); 332 int line = y + 2; 333 if (c->intent == I_RESPOND) { 334 tk_att(tk_xterm(C_TEXT), 236, 0); 335 tk_put(line, x + 2, ctrl_prompt(c), w - 4); 336 line += 2; 337 } 338 for (int i = 0; i < c->nitems; i++) { 339 const MenuItem *it = &c->items[i]; 340 char buf[80]; 341 snprintf(buf, sizeof buf, " %c %s", it->key, it->label); 342 if (i == c->icursor) tk_att(tk_xterm(C_BLACK), tk_xterm(C_ACCENT), A_BOLD); 343 else tk_att(it->enabled ? 255 : 242, 236, 0); 344 tk_fill(line, x + 2, w - 4, " "); 345 tk_put(line, x + 2, buf, w - 4); 346 hit_add(&f->hits, x + 2, line, w - 4, 1, PK_ITEM, i); 347 line++; 348 } 349 } 350 351 static void draw_cards_modal(Frontend *f, const Game *g, const Ctrl *c, int bx, int by, int bw, int bh) 352 { 353 bool offer = c->intent == I_OFFER || c->intent == I_COUNTER; 354 int w = 52, h = offer ? 12 : 10; 355 int x = bx + (bw - w) / 2, y = by + (bh - h) / 2; 356 tk_box(y, x, h, w, ctrl_prompt(c), C_ACCENT); 357 int cx0 = x + 10; 358 for (int r = 0; r < NRES; r++) { 359 tk_attc(C_BLACK, TERRAIN_COLOR[r], A_BOLD); 360 char buf[12]; 361 snprintf(buf, sizeof buf, " %-5.5s", res_short(r)); 362 mvaddstr(y + 2, cx0 + r * 8, buf); 363 } 364 int rows = offer ? 2 : 1; 365 for (int row = 0; row < rows; row++) { 366 int ly = y + 3 + row; 367 tk_att(255, 236, row == c->row && offer ? A_BOLD : 0); 368 tk_put(ly, x + 2, offer ? (row ? tr(S_GET) : tr(S_GIVE)) : "", 7); 369 const uint8_t *sel = row ? c->sel2 : c->sel; 370 for (int r = 0; r < NRES; r++) { 371 bool curcell = row == c->row && r == c->rescur; 372 int cx = cx0 + r * 8; 373 tk_att(250, 236, 0); 374 mvaddstr(ly, cx, "-"); 375 hit_add(&f->hits, cx, ly, 1, 1, PK_CARD, row * NRES * 2 + r * 2 + 1); 376 char num[8]; 377 snprintf(num, sizeof num, "%2d", sel[r]); 378 if (curcell) tk_att(tk_xterm(C_BLACK), tk_xterm(C_CURSOR), A_BOLD); 379 else tk_att(sel[r] ? 255 : 244, 236, sel[r] ? A_BOLD : 0); 380 mvaddstr(ly, cx + 1, num); 381 tk_att(250, 236, 0); 382 mvaddstr(ly, cx + 3, "+"); 383 hit_add(&f->hits, cx + 3, ly, 1, 1, PK_CARD, row * NRES * 2 + r * 2); 384 } 385 } 386 int ly = y + 3 + rows; 387 tk_att(246, 236, 0); 388 tk_put(ly, x + 2, g->p[c->actor].neutral ? "" : (g_lang ? "Have" : "Tenés"), 7); 389 for (int r = 0; r < NRES; r++) { 390 char num[8]; 391 snprintf(num, sizeof num, "%2d", (c->intent == I_PLENTY || c->intent == I_GOLD) ? g->bank[r] : g->p[c->actor].res[r]); 392 mvaddstr(ly, cx0 + r * 8 + 1, num); 393 } 394 tk_att(244, 236, 0); 395 tk_put(y + h - 4, x + 2, g_lang ? "←→ resource ↑↓/+- amount 1-5 add" : "←→ recurso ↑↓/+- cantidad 1-5 suma", w - 4); 396 if (offer) tk_put(y + h - 3, x + 2, g_lang ? "Tab: switch give/get row" : "Tab: cambiar fila das/pedís", w - 4); 397 tk_att(tk_xterm(C_BLACK), tk_xterm(C_OK), A_BOLD); 398 mvaddstr(y + h - 2, x + w - 22, " OK "); 399 hit_add(&f->hits, x + w - 22, y + h - 2, 4, 1, PK_CARD, 100); 400 tk_att(tk_xterm(C_BLACK), 250, 0); 401 mvaddstr(y + h - 2, x + w - 16, g_lang ? " Cancel " : " Cancelar "); 402 hit_add(&f->hits, x + w - 16, y + h - 2, 10, 1, PK_CARD, 101); 403 } 404 405 static void draw_over(const Game *g, const UiInfo *ui, int bx, int by, int bw, int bh) 406 { 407 int w = 48, h = g->o.np + 6 + (ui->can_free ? 2 : 0) + (ui->can_again ? 1 : 0); 408 int x = bx + (bw - w) / 2, y = by + (bh - h) / 2; 409 tk_box(y, x, h, w, tr(S_P_OVER), C_ACCENT); 410 char buf[96]; 411 if (g->winner >= 0) { 412 snprintf(buf, sizeof buf, "★ %s %s ★", ui->names[g->winner], tr(S_WINS)); 413 tk_att(tk_xterm(C_ACCENT), 236, A_BOLD); 414 tk_put(y + 2, x + (w - tk_ulen(buf)) / 2, buf, w - 4); 415 } 416 for (int p = 0; p < g->o.np; p++) { 417 if (g->p[p].neutral) continue; 418 snprintf(buf, sizeof buf, "%-16s %2d %s", ui->names[p], game_vp(g, p), tr(S_VP)); 419 tk_attc(PLAYER_COLOR[p], RGB(48, 48, 48), 0); 420 mvaddstr(y + 4 + p, x + 3, "██"); 421 tk_att(255, 236, p == g->winner ? A_BOLD : 0); 422 tk_put(y + 4 + p, x + 6, buf, w - 8); 423 } 424 int ly = y + 5 + g->o.np; 425 if (ui->can_free) { 426 tk_att(tk_xterm(C_OK), 236, 0); 427 tk_put(ly, x + (w - tk_ulen(tr(S_GO_FREE))) / 2, tr(S_GO_FREE), w - 4); 428 ly++; 429 } 430 if (ui->can_again) { 431 tk_att(tk_xterm(C_ACCENT), 236, 0); 432 tk_put(ly, x + (w - tk_ulen(tr(S_AGAIN))) / 2, tr(S_AGAIN), w - 4); 433 } 434 } 435 436 static void draw_help(int bx, int by, int bw, int bh) 437 { 438 static const char *ES[] = { 439 "Enter / Espacio confirmar, abrir acción", 440 "↑↓←→ / hjkl mover el cursor (Tab: próximo válido)", 441 "Esc volver / cancelar", 442 "r tirar los dados", 443 "1 2 3 4 ruta, pueblo, ciudad, carta", 444 "d jugar carta de desarrollo", 445 "b comerciar con banco/puerto", 446 "t ofrecer trueque", 447 "o contraofertar (al responder)", 448 "F2 / click abajo chat (/w nombre: privado)", 449 "RePág AvPág Fin recorrer el registro", 450 "F5 exportar la partida (texto)", 451 "e terminar el turno", 452 "u deshacer (construcciones y banco)", 453 "Tab siguiente objetivo", 454 "L cambiar idioma", 455 "q salir (la partida se guarda)", 456 "Mouse click en el tablero y menús" }; 457 static const char *EN[] = { 458 "Enter / Space confirm, open action", 459 "↑↓←→ / hjkl move the cursor", 460 "Esc back / cancel", 461 "r roll the dice", 462 "1 2 3 4 road, settlement, city, card", 463 "d play a development card", 464 "b trade with bank/port", 465 "t offer a trade", 466 "o counter-offer (when answering)", 467 "F2 / click below chat (/w name: private)", 468 "PgUp PgDn End scroll the log", 469 "F5 export the game (text)", 470 "e end the turn", 471 "u undo (builds and bank trades)", 472 "Tab next target", 473 "L switch language", 474 "q quit (the game is saved)", 475 "Mouse click on the board and menus" }; 476 const char **L = g_lang ? EN : ES; 477 int n = 18, w = 56; 478 bool cards = bh >= n + 1 + NDEV + 4; /* que hace cada carta, si la terminal es alta */ 479 int h = n + (cards ? 1 + NDEV : 0) + 4; 480 int x = bx + (bw - w) / 2, y = by + (bh - h) / 2; 481 tk_box(y, x, h, w, g_lang ? "Help" : "Ayuda", C_ACCENT); 482 tk_att(255, 236, 0); 483 for (int i = 0; i < n; i++) tk_put(y + 2 + i, x + 3, L[i], w - 6); 484 if (cards) { 485 tk_att(tk_xterm(C_ACCENT), 236, A_BOLD); 486 tk_put(y + 2 + n, x + 3, tr(S_DEV_CARDS), w - 6); 487 tk_att(255, 236, 0); 488 for (int d = 0; d < NDEV; d++) { 489 char t[120]; 490 snprintf(t, sizeof t, "%-18s %s", dev_name(d), tr(S_DD_KNIGHT + d)); 491 tk_put(y + 3 + n + d, x + 3, t, w - 6); 492 } 493 } 494 } 495 496 static void draw_confirm(const UiInfo *ui, int bx, int by, int bw, int bh) 497 { 498 int w = 46, h = 6; 499 int x = bx + (bw - w) / 2, y = by + (bh - h) / 2; 500 tk_box(y, x, h, w, "?", C_ERR); 501 tk_att(255, 236, A_BOLD); 502 tk_put(y + 2, x + 3, ui->modal_text ? ui->modal_text : "", w - 6); 503 tk_att(250, 236, 0); 504 tk_put(y + 3, x + 3, g_lang ? "[y] yes [n] no" : "[s] sí [n] no", w - 6); 505 } 506 507 /* ------------------------------------------------------------------ pantalla */ 508 509 static void tui_draw_game(Frontend *f, const Game *g, const Ctrl *c, const UiInfo *ui) 510 { 511 f->hits.n = 0; 512 tk_clear(); 513 int H = LINES, W = COLS; 514 if (W < 80 || H < 24) { 515 tk_att(tk_xterm(C_ERR), TK_DEF, A_BOLD); 516 mvaddstr(0, 0, g_lang ? "Make the terminal bigger (80x24 min)" : "Agrandá la terminal (mínimo 80x24)"); 517 refresh(); 518 return; 519 } 520 int PW = W >= 140 ? 48 : W >= 110 ? 42 : 36; 521 int BW = W - PW - 1, BH = H - 3; 522 523 /* barra de titulo */ 524 char buf[160]; 525 tk_att(tk_xterm(C_BLACK), tk_xterm(C_ACCENT), A_BOLD); 526 tk_fill(0, 0, W, " "); 527 mvaddstr(0, 1, "CATAN"); 528 int n = snprintf(buf, sizeof buf, " %s %d", g_lang ? "Turn" : "Turno", g->turn + 1); 529 if (g->dice[0]) n += snprintf(buf + n, sizeof buf - (size_t)n, " · %s %d+%d", tr(S_DICE), g->dice[0], g->dice[1]); 530 if (g->dice[2]) n += snprintf(buf + n, sizeof buf - (size_t)n, " / %d+%d", g->dice[2], g->dice[3]); 531 if (g->winner >= 0 && g->phase != PH_OVER) 532 n += snprintf(buf + n, sizeof buf - (size_t)n, " · ★ %s %s", ui->names[g->winner], tr(S_WON_FREE)); 533 tk_att(tk_xterm(C_BLACK), tk_xterm(C_ACCENT), 0); 534 tk_put(0, 7, buf, W - 8); 535 snprintf(buf, sizeof buf, "%s %d · %s", tr(S_DECK), g->deck_n, tr(S_BANK)); 536 int bx = W - 1 - tk_ulen(buf) - NRES * 4; 537 tk_put(0, bx, buf, W); 538 bx += tk_ulen(buf) + 1; 539 for (int r = 0; r < NRES; r++) { 540 char nb[8]; 541 snprintf(nb, sizeof nb, "%2d", g->bank[r]); 542 tk_attc(C_BLACK, TERRAIN_COLOR[r], A_BOLD); 543 mvaddstr(0, bx, nb); 544 bx += 3; 545 } 546 if (ui->status && *ui->status) { 547 tk_att(tk_xterm(C_BLACK), tk_xterm(C_ACCENT), 0); 548 tk_put(0, 40, ui->status, bx - 40 - NRES * 3 - 12); 549 } 550 551 draw_board(f, g, c, 0, 1, BW, BH); 552 draw_panel(f, g, c, ui, BW + 1, 1, PW, BH); 553 554 /* instruccion y mensajes */ 555 tk_att(tk_xterm(C_ACCENT), TK_DEF, A_BOLD); 556 tk_fill(H - 2, 0, W, " "); 557 mvaddstr(H - 2, 1, "▶ "); 558 tk_put(H - 2, 3, ui->preview ? ui->preview : ctrl_prompt(c), W - 4); 559 tk_fill(H - 1, 0, W, " "); 560 if (ui->preview) { 561 tk_att(244, TK_DEF, 0); 562 tk_put(H - 1, 1, g_lang ? "R reshuffle · ← → change map · Enter play · Esc back" 563 : "R otro reparto · ← → cambiar de mapa · Enter jugar · Esc volver", W - 2); 564 } else if (c->msg[0]) { tk_att(tk_xterm(c->msg_err ? C_ERR : C_OK), TK_DEF, A_BOLD); tk_put(H - 1, 1, c->msg, W - 2); } 565 else if (ui->chat_focus) { 566 tk_att(244, TK_DEF, 0); 567 tk_put(H - 1, 1, g_lang ? "Enter send · Esc cancel · /w name text: private" : "Enter manda · Esc cancela · /w nombre texto: privado", W - 2); 568 } else { 569 tk_att(244, TK_DEF, 0); 570 tk_put(H - 1, 1, g_lang ? "Enter action · arrows move · Esc back · ? help · F2 chat · PgUp log · L language · q quit" 571 : "Enter acción · flechas mover · Esc volver · ? ayuda · F2 chat · RePág registro · L idioma · q salir", W - 2); 572 } 573 574 if (c->mode == CM_MENU) draw_menu_modal(f, c, 0, 1, BW, BH); 575 else if (c->mode == CM_CARDS) draw_cards_modal(f, g, c, 0, 1, BW, BH); 576 else if (c->mode == CM_HANDOFF) { 577 int w = 50, h = 7, x = (BW - w) / 2, y = 1 + (BH - h) / 2; 578 tk_box(y, x, h, w, "", C_ACCENT); 579 snprintf(buf, sizeof buf, "%s %s", tr(S_P_HANDOFF), ui->names[c->actor]); 580 tk_attc(PLAYER_COLOR[c->actor], RGB(48, 48, 48), A_BOLD); 581 tk_put(y + 2, x + (w - tk_ulen(buf)) / 2, buf, w - 4); 582 tk_att(250, 236, 0); 583 tk_put(y + 4, x + (w - tk_ulen(tr(S_P_ENTER))) / 2, tr(S_P_ENTER), w - 4); 584 hit_add(&f->hits, x, y, w, h, PK_ITEM, 0); 585 } else if (c->mode == CM_OVER || g->phase == PH_OVER) draw_over(g, ui, 0, 1, BW, BH); 586 if (ui->show_help) draw_help(0, 1, BW, BH); 587 if (ui->modal) draw_confirm(ui, 0, 1, BW, BH); 588 refresh(); 589 } 590 591 static void tui_draw_form(Frontend *f, const Form *form) { tk_draw_form(form, &f->hits); } 592 593 /* logo de los menus: cinco fichas con las letras */ 594 static void form_logo(int cols) 595 { 596 static const char *LOGO[] = { "C", "A", "T", "A", "N" }; 597 int lx = cols / 2 - 9; 598 for (int i = 0; i < 5; i++) { 599 tk_attc(C_BLACK, TERRAIN_COLOR[i], A_BOLD); 600 char b[8]; 601 snprintf(b, sizeof b, " %s ", LOGO[i]); 602 mvaddstr(1, lx + i * 4, b); 603 } 604 } 605 606 static int tui_wait(Frontend *f, int timeout_ms, const int *fds, int nfds, InEvent *ev) 607 { 608 (void)f; 609 return tk_wait(timeout_ms, fds, nfds, ev); 610 } 611 612 static void tui_close(Frontend *f) 613 { 614 (void)f; 615 tk_close(); 616 } 617 618 Frontend *tui_open(void) 619 { 620 static Frontend fe; 621 tk_open(); 622 tk_form_logo = form_logo; 623 memset(&fe, 0, sizeof fe); 624 fe.draw_game = tui_draw_game; 625 fe.draw_form = tui_draw_form; 626 fe.wait = tui_wait; 627 fe.close = tui_close; 628 return &fe; 629 }