diff --git a/data/font/aseprite.fnt b/data/font/aseprite.fnt index a29a6f1..001751d 100644 --- a/data/font/aseprite.fnt +++ b/data/font/aseprite.fnt @@ -1,135 +1,134 @@ -# Font: aseprite -# Formato: codepoint_decimal ancho_visual -# Los gliphos se listan en orden de aparición en el bitmap (izquierda→derecha, arriba→abajo) +# Font: aseprite — generado desde aseprite_font.gif +# Generado con tools/font_gen/font_gen.py -box_width 8 -box_height 8 -columns 15 +box_width 10 +box_height 7 +columns 16 +cell_spacing 1 +row_spacing 4 -# ASCII 32-126 -32 3 -33 1 -34 3 -35 3 -36 4 -37 5 -38 5 -39 2 -40 2 -41 2 -42 5 -43 5 -44 3 -45 3 -46 1 -47 4 -48 4 -49 2 -50 4 -51 4 -52 4 -53 4 -54 4 -55 4 -56 4 -57 4 -58 1 -59 1 -60 3 -61 4 -62 4 -63 4 -64 7 -65 4 -66 4 -67 4 -68 4 -69 4 -70 4 -71 4 -72 4 -73 2 -74 2 -75 4 -76 4 -77 5 -78 4 -79 5 -80 4 -81 5 -82 4 -83 4 -84 5 -85 4 -86 5 -87 7 -88 5 -89 5 -90 4 -91 2 -92 3 -93 2 -94 5 -95 5 -96 3 -97 4 -98 4 -99 4 -100 4 -101 4 -102 2 -103 4 -104 4 -105 1 -106 2 -107 4 -108 1 -109 7 -110 4 -111 4 -112 4 -113 4 -114 3 -115 3 -116 2 -117 4 -118 4 -119 5 -120 5 -121 4 -122 4 -123 3 -124 3 -125 3 -126 5 - -# Extensiones para ES/CA/VA (descomentar tras añadirlos al bitmap) -# 192 4 # À -# 193 4 # Á -# 200 4 # È -# 201 4 # É -# 205 2 # Í -# 207 2 # Ï -# 210 5 # Ò -# 211 5 # Ó -# 218 4 # Ú -# 220 4 # Ü -# 209 4 # Ñ -# 199 4 # Ç -# 224 4 # à -# 225 4 # á -# 232 4 # è -# 233 4 # é -# 237 1 # í -# 239 2 # ï -# 242 4 # ò -# 243 4 # ó -# 250 4 # ú -# 252 4 # ü -# 241 4 # ñ -# 231 4 # ç -# 161 1 # ¡ -# 191 4 # ¿ -# 171 5 # « -# 187 5 # » -# 183 1 # · (punt volat) +# codepoint_decimal ancho_visual +32 3 # U+0020 +33 1 # ! +34 3 # " +35 5 # # +36 4 # $ +37 5 # % +38 5 # & +39 2 # ' +40 2 # ( +41 2 # ) +42 5 # * +43 5 # + +44 2 # , +45 3 # - +46 1 # . +47 3 # / +48 4 # 0 +49 2 # 1 +50 4 # 2 +51 4 # 3 +52 4 # 4 +53 4 # 5 +54 4 # 6 +55 4 # 7 +56 4 # 8 +57 4 # 9 +58 1 # : +59 2 # ; +60 3 # < +61 4 # = +62 3 # > +63 4 # ? +64 8 # @ +65 4 # A +66 4 # B +67 4 # C +68 4 # D +69 4 # E +70 4 # F +71 4 # G +72 4 # H +73 1 # I +74 2 # J +75 4 # K +76 4 # L +77 5 # M +78 4 # N +79 5 # O +80 4 # P +81 5 # Q +82 4 # R +83 4 # S +84 5 # T +85 4 # U +86 5 # V +87 7 # W +88 5 # X +89 5 # Y +90 4 # Z +91 2 # [ +92 3 # \ +93 2 # ] +94 5 # ^ +95 5 # _ +96 3 # ` +97 4 # a +98 4 # b +99 4 # c +100 4 # d +101 4 # e +102 2 # f +103 4 # g +104 4 # h +105 1 # i +106 2 # j +107 4 # k +108 1 # l +109 7 # m +110 4 # n +111 4 # o +112 4 # p +113 4 # q +114 3 # r +115 3 # s +116 2 # t +117 4 # u +118 4 # v +119 5 # w +120 5 # x +121 4 # y +122 4 # z +123 3 # { +124 1 # | +125 3 # } +126 4 # ~ +192 5 # À +193 5 # Á +200 5 # È +201 5 # É +205 5 # Í +207 5 # Ï +210 5 # Ò +211 5 # Ó +218 5 # Ú +220 5 # Ü +209 5 # Ñ +199 5 # Ç +224 5 # à +225 5 # á +232 5 # è +233 5 # é +237 5 # í +239 5 # ï +242 5 # ò +243 5 # ó +250 5 # ú +252 5 # ü +241 5 # ñ +231 5 # ç +161 5 # ¡ +191 5 # ¿ +171 5 # « +187 5 # » +183 5 # · diff --git a/data/font/aseprite.gif b/data/font/aseprite.gif index 3ebd454..9c0c9f1 100644 Binary files a/data/font/aseprite.gif and b/data/font/aseprite.gif differ diff --git a/data/locale/ca.yaml b/data/locale/ca.yaml index 59c98a2..ac8d34e 100644 --- a/data/locale/ca.yaml +++ b/data/locale/ca.yaml @@ -59,9 +59,9 @@ ending2: credits: instructions: "INSTRUCCIONS:" - l0: "AJUDA EL JAILDOC A RECUPERAR" - l1: "ELS SEUS PROJECTES I ARRIBAR A" - l2: "LA JAIL PER ACABAR-LOS" + l0: "AJUDA A JAILDOC A RECUPERAR" + l1: "ELS SEUS PROJECTES I ARRIBAR" + l2: "A LA JAIL PER ACABAR-LOS" keys: "TECLES:" keys_move: "CURSORS PER A MOURE I SALTAR" f8: "F8 ACTIVAR/DESACTIVAR MÚSICA" @@ -119,7 +119,7 @@ ui: scoreboard: items: "TRESORS PILLATS " - time: " TEMPS " + time: " HORA " rooms: "SALES" game: diff --git a/source/core/rendering/text.cpp b/source/core/rendering/text.cpp index 0e13ef0..402336b 100644 --- a/source/core/rendering/text.cpp +++ b/source/core/rendering/text.cpp @@ -86,6 +86,10 @@ auto Text::loadTextFile(const std::string& file_path) -> std::shared_ptr { ls >> tf->box_height; } else if (key == "columns") { ls >> tf->columns; + } else if (key == "cell_spacing") { + ls >> tf->cell_spacing; + } else if (key == "row_spacing") { + ls >> tf->row_spacing; } else { // Línea de glifo: codepoint_decimal ancho_visual uint32_t codepoint = 0; @@ -97,8 +101,9 @@ auto Text::loadTextFile(const std::string& file_path) -> std::shared_ptr { continue; // línea mal formateada, ignorar } Offset off{}; - off.x = (glyph_index % tf->columns) * tf->box_width; - off.y = (glyph_index / tf->columns) * tf->box_height; + const int row_sp = tf->row_spacing > 0 ? tf->row_spacing : tf->cell_spacing; + off.x = (glyph_index % tf->columns) * (tf->box_width + tf->cell_spacing) + tf->cell_spacing; + off.y = (glyph_index / tf->columns) * (tf->box_height + row_sp) + tf->cell_spacing; off.w = width; tf->offset[codepoint] = off; ++glyph_index; diff --git a/source/core/rendering/text.hpp b/source/core/rendering/text.hpp index 6b99497..2211f06 100644 --- a/source/core/rendering/text.hpp +++ b/source/core/rendering/text.hpp @@ -18,9 +18,11 @@ class Text { }; struct File { - int box_width{0}; // Anchura de la caja de cada caracter en el png - int box_height{0}; // Altura de la caja de cada caracter en el png - int columns{16}; // Número de columnas en el bitmap + int box_width{0}; // Anchura de la caja de cada caracter en el png + int box_height{0}; // Altura de la caja de cada caracter en el png + int columns{16}; // Número de columnas en el bitmap + int cell_spacing{0}; // Píxeles de separación entre columnas (y borde izquierdo/superior) + int row_spacing{0}; // Píxeles de separación entre filas (si difiere de cell_spacing) std::unordered_map offset; // Posición y ancho de cada glifo (clave: codepoint Unicode) }; diff --git a/tools/font_gen/font_gen.py b/tools/font_gen/font_gen.py index b939a48..3f9d06e 100644 --- a/tools/font_gen/font_gen.py +++ b/tools/font_gen/font_gen.py @@ -1,24 +1,30 @@ #!/usr/bin/env python3 """Generador de fuentes bitmap para JailDoctor's Dilemma. -Convierte un archivo .ttf en un GIF indexado + fichero .fnt compatibles -con el sistema de texto del juego. +Convierte un archivo .ttf (o un GIF existente) en un GIF indexado + fichero .fnt +compatibles con el sistema de texto del juego. Dependencias: pip install Pillow -Uso: +Uso (desde TTF): python3 font_gen.py --ttf myfont.ttf --size 8 --output myfont python3 font_gen.py --ttf myfont.ttf --size 8 --output myfont --dir data/font --box-width 8 +Uso (desde GIF existente con cuadrícula): + python3 font_gen.py --gif myfont.gif --output myfont --columns 16 --cell-spacing 1 + Notas: - Para fuentes bitmap (pixel fonts) en TTF, usa el tamaño exacto del bitmap strike. - Los glifos se almacenan como índice de paleta 1 (blanco) sobre fondo transparente (índice 0). - Esto es compatible con SurfaceSprite::render(1, color) del motor del juego. - Los caracteres no incluidos en la fuente aparecerán como celdas vacías en el GIF. + - El modo --gif acepta un GIF cuya cuadrícula siga el orden de ALL_CHARS. + Celdas vacías (todos los píxeles = índice 0) se marcan como no soportadas. """ import argparse import os +import shutil import sys from math import ceil @@ -35,6 +41,124 @@ _EXTENDED_CHARS = list("ÀÁÈÉÍÏÒÓÚÜÑÇàáèéíïòóúüñç¡¿«» ALL_CHARS = _ASCII_CHARS + _EXTENDED_CHARS # 124 total +def _write_fnt( + output_fnt: str, + output_name: str, + source_name: str, + box_width: int, + box_height: int, + columns: int, + cell_spacing: int, + row_spacing: int, + chars: list[str], + char_widths: dict[str, int], +) -> None: + """Escribe el fichero .fnt.""" + with open(output_fnt, "w", encoding="utf-8") as f: + f.write(f"# Font: {output_name} — generado desde {source_name}\n") + f.write(f"# Generado con tools/font_gen/font_gen.py\n\n") + f.write(f"box_width {box_width}\n") + f.write(f"box_height {box_height}\n") + f.write(f"columns {columns}\n") + if cell_spacing: + f.write(f"cell_spacing {cell_spacing}\n") + if row_spacing and row_spacing != cell_spacing: + f.write(f"row_spacing {row_spacing}\n") + f.write("\n# codepoint_decimal ancho_visual\n") + for ch in chars: + cp = ord(ch) + w = char_widths[ch] + name = ch if ch.isprintable() and not ch.isspace() else f"U+{cp:04X}" + f.write(f"{cp} {w} # {name}\n") + + +def render_gif_font( + gif_path: str, + output_name: str, + output_dir: str, + columns: int, + box_width: int, + box_height: int, + cell_spacing: int, + row_spacing: int, +) -> None: + """Genera el .fnt a partir de un GIF de fuente existente con cuadrícula. + + Fórmula de posición de cada celda (col, row): + x0 = col * (box_width + cell_spacing) + cell_spacing + y0 = row * (box_height + row_spacing) + cell_spacing + """ + + if not os.path.isfile(gif_path): + print(f"Error: No se encuentra el archivo GIF: {gif_path}", file=sys.stderr) + sys.exit(1) + + os.makedirs(output_dir, exist_ok=True) + output_gif = os.path.join(output_dir, f"{output_name}.gif") + output_fnt = os.path.join(output_dir, f"{output_name}.fnt") + + img = Image.open(gif_path) + if img.mode != "P": + img = img.convert("P") + img_w, img_h = img.size + pixels = img.load() + + num_rows = ceil(len(ALL_CHARS) / columns) + stride_x = box_width + cell_spacing + stride_y = box_height + row_spacing + + print(f"GIF fuente: {os.path.basename(gif_path)} ({img_w}×{img_h} px)") + print(f"Caja : {box_width}×{box_height} px | {columns} cols, {num_rows} filas | spacing x={cell_spacing} y={row_spacing}") + + char_widths: dict[str, int] = {} + supported_chars: list[str] = [] + skipped: list[str] = [] + + for i, ch in enumerate(ALL_CHARS): + col = i % columns + row = i // columns + x0 = col * stride_x + cell_spacing + y0 = row * stride_y + cell_spacing + + # Comprobar límites (el GIF puede tener más o menos filas que ALL_CHARS) + if y0 + box_height > img_h or x0 + box_width > img_w: + skipped.append(ch) + continue + + # Celda vacía (todos los píxeles = índice 0) + if all(pixels[x0 + px, y0 + py] == 0 for py in range(box_height) for px in range(box_width)): + if ch.isspace(): + # El espacio no tiene píxeles visibles: asignar ancho por defecto + char_widths[ch] = max(1, box_width // 2) + supported_chars.append(ch) + else: + skipped.append(ch) + continue + + # Medir ancho visual: última columna con algún píxel no-fondo + pixel_width = 0 + for px in range(box_width - 1, -1, -1): + if any(pixels[x0 + px, y0 + py] != 0 for py in range(box_height)): + pixel_width = px + 1 + break + + char_widths[ch] = max(1, pixel_width) + supported_chars.append(ch) + + if skipped: + names = "".join(c if c.isprintable() and not c.isspace() else f"[U+{ord(c):04X}]" for c in skipped) + print(f"Omitidos : {len(skipped)} chars vacíos/fuera de rango: {names}") + print(f"Soportados: {len(supported_chars)} caracteres") + + # Copiar el GIF al directorio de salida + shutil.copy2(gif_path, output_gif) + print(f"GIF : {output_gif}") + + _write_fnt(output_fnt, output_name, os.path.basename(gif_path), + box_width, box_height, columns, cell_spacing, row_spacing, supported_chars, char_widths) + print(f"FNT : {output_fnt}") + + def render_font( ttf_path: str, size: int, @@ -176,21 +300,8 @@ def render_font( pass # si magick no está disponible se queda como GIF87a de 256 colores print(f"GIF : {output_gif}") - # --- Generar fichero .fnt --- - ttf_name = os.path.basename(ttf_path) - with open(output_fnt, "w", encoding="utf-8") as f: - f.write(f"# Font: {output_name} — generado desde {ttf_name} size {size}\n") - f.write(f"# Generado con tools/font_gen/font_gen.py\n\n") - f.write(f"box_width {box_width}\n") - f.write(f"box_height {box_height}\n") - f.write(f"columns {columns}\n\n") - f.write("# codepoint_decimal ancho_visual\n") - for ch in chars_to_render: - cp = ord(ch) - w = char_widths[ch] - name = ch if ch.isprintable() and not ch.isspace() else f"U+{cp:04X}" - f.write(f"{cp} {w} # {name}\n") - + _write_fnt(output_fnt, output_name, f"{os.path.basename(ttf_path)} size {size}", + box_width, box_height, columns, 0, 0, chars_to_render, char_widths) print(f"FNT : {output_fnt}") @@ -201,41 +312,71 @@ def main() -> None: default_dir = os.path.join(project_dir, "data", "font") parser = argparse.ArgumentParser( - description="Genera fuentes bitmap (.gif + .fnt) desde un archivo .ttf.", + description="Genera fuentes bitmap (.gif + .fnt) desde un .ttf o un GIF existente.", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" -Ejemplos: +Ejemplos (desde TTF): python3 font_gen.py --ttf myfont.ttf --size 8 --output myfont python3 font_gen.py --ttf myfont.ttf --size 8 --output myfont --box-width 8 python3 font_gen.py --ttf myfont.ttf --size 16 --output myfont --dir data/font +Ejemplos (desde GIF con cuadrícula): + python3 font_gen.py --gif myfont.gif --output myfont --columns 16 --box-width 10 --box-height 7 --cell-spacing 1 + python3 font_gen.py --gif myfont.gif --output myfont --columns 16 --box-width 10 --box-height 7 --cell-spacing 1 --row-spacing 4 + Notas: - Para pixel fonts (.ttf bitmap), usa el tamaño exacto del bitmap strike. - El GIF resultante usa índice 1 = glifo, índice 0 = transparente. - - Se generan 124 caracteres: ASCII 32-126 + extensiones ES/CA/VA. - - Usa --box-width para forzar una anchura de celda fija (útil para fuentes - cuadradas donde la celda coincide con box_height). + - Se procesan 124 caracteres: ASCII 32-126 + extensiones ES/CA/VA. + - En modo --gif, --box-width y --box-height son obligatorios. + - Las celdas completamente vacías (índice 0) se marcan como no soportadas. + - cell_spacing = spacing horizontal entre columnas (y borde izquierdo/superior). + - row_spacing = spacing vertical entre filas (si difiere de cell_spacing). """, ) - parser.add_argument("--ttf", required=True, help="Ruta al archivo .ttf") - parser.add_argument("--size", required=True, type=int, help="Tamaño en píxeles") - parser.add_argument("--output", required=True, help="Nombre base de salida (sin extensión)") - parser.add_argument("--dir", default=default_dir, help="Directorio de salida (default: data/font/)") - parser.add_argument("--columns", default=15, type=int, help="Columnas en el bitmap (default: 15)") - parser.add_argument("--box-width", default=None, type=int, help="Anchura fija de celda en px (default: auto)") - parser.add_argument("--box-height", default=None, type=int, help="Altura fija de celda en px (default: ascent + abs(descent))") + + # Fuente: TTF o GIF (mutuamente excluyentes) + source_group = parser.add_mutually_exclusive_group(required=True) + source_group.add_argument("--ttf", help="Ruta al archivo .ttf") + source_group.add_argument("--gif", help="Ruta a un GIF de fuente existente con cuadrícula") + + parser.add_argument("--size", type=int, help="Tamaño en píxeles (solo con --ttf)") + parser.add_argument("--output", required=True, help="Nombre base de salida (sin extensión)") + parser.add_argument("--dir", default=default_dir, help="Directorio de salida (default: data/font/)") + parser.add_argument("--columns", default=15, type=int, help="Columnas en el bitmap (default: 15)") + parser.add_argument("--box-width", default=None, type=int, help="Anchura fija de celda en px (default: auto)") + parser.add_argument("--box-height", default=None, type=int, help="Altura fija de celda en px (default: auto)") + parser.add_argument("--cell-spacing", default=0, type=int, help="Píxeles de separación entre columnas, y borde (default: 0)") + parser.add_argument("--row-spacing", default=None, type=int, help="Píxeles de separación entre filas (default: igual a --cell-spacing)") args = parser.parse_args() - render_font( - ttf_path = args.ttf, - size = args.size, - output_name = args.output, - output_dir = args.dir, - columns = args.columns, - box_width_override = args.box_width, - box_height_override = args.box_height, - ) + if args.gif: + if args.box_width is None or args.box_height is None: + parser.error("--box-width y --box-height son obligatorios con --gif") + row_sp = args.row_spacing if args.row_spacing is not None else args.cell_spacing + render_gif_font( + gif_path = args.gif, + output_name = args.output, + output_dir = args.dir, + columns = args.columns, + box_width = args.box_width, + box_height = args.box_height, + cell_spacing = args.cell_spacing, + row_spacing = row_sp, + ) + else: + if args.size is None: + parser.error("--size es obligatorio cuando se usa --ttf") + render_font( + ttf_path = args.ttf, + size = args.size, + output_name = args.output, + output_dir = args.dir, + columns = args.columns, + box_width_override = args.box_width, + box_height_override = args.box_height, + ) if __name__ == "__main__":