Files
coffee_crisis_arcade_edition/source/text.cpp
2025-10-19 22:01:31 +02:00

444 lines
17 KiB
C++

#include "text.hpp"
#include <SDL3/SDL.h> // Para SDL_FRect, Uint8, SDL_GetRenderTarget, SDL_RenderClear, SDL_SetRenderDrawColor, SDL_SetRenderTarget, SDL_BLENDMODE_BLEND, SDL_PixelFormat, SDL_TextureAccess, SDL_GetTextureAlphaMod
#include <fstream> // Para basic_ifstream, basic_istream, basic_ostream, operator<<, istream, ifstream, istringstream
#include <iostream> // Para cerr
#include <sstream> // Para basic_istringstream
#include <stdexcept> // Para runtime_error
#include <string_view> // Para string_view
#include <vector> // Para vector
#include "color.hpp" // Para Color
#include "resource_helper.hpp" // Para loadFile
#include "screen.hpp" // Para Screen
#include "sprite.hpp" // Para Sprite
#include "texture.hpp" // Para Texture
#include "ui/logger.hpp" // Para dots
#include "utils.hpp" // Para getFileName
// Constructor
Text::Text(const std::shared_ptr<Texture>& texture, const std::string& text_file) {
// Carga los offsets desde el fichero
auto tf = loadFile(text_file);
// Inicializa variables desde la estructura
box_height_ = tf->box_height;
box_width_ = tf->box_width;
for (int i = 0; i < 128; ++i) {
offset_[i].x = tf->offset[i].x;
offset_[i].y = tf->offset[i].y;
offset_[i].w = tf->offset[i].w;
}
// Crea los objetos
sprite_ = std::make_unique<Sprite>(texture, (SDL_FRect){0, 0, static_cast<float>(box_width_), static_cast<float>(box_height_)});
// Inicializa variables
fixed_width_ = false;
}
// Constructor
Text::Text(const std::shared_ptr<Texture>& texture, const std::shared_ptr<Text::File>& text_file) {
// Inicializa variables desde la estructura
box_height_ = text_file->box_height;
box_width_ = text_file->box_width;
for (int i = 0; i < 128; ++i) {
offset_[i].x = text_file->offset[i].x;
offset_[i].y = text_file->offset[i].y;
offset_[i].w = text_file->offset[i].w;
}
// Crea los objetos
sprite_ = std::make_unique<Sprite>(texture, (SDL_FRect){0, 0, static_cast<float>(box_width_), static_cast<float>(box_height_)});
// Inicializa variables
fixed_width_ = false;
}
// Constructor con textura blanca opcional
Text::Text(const std::shared_ptr<Texture>& texture, const std::shared_ptr<Texture>& white_texture, const std::string& text_file) {
// Carga los offsets desde el fichero
auto tf = loadFile(text_file);
// Inicializa variables desde la estructura
box_height_ = tf->box_height;
box_width_ = tf->box_width;
for (int i = 0; i < 128; ++i) {
offset_[i].x = tf->offset[i].x;
offset_[i].y = tf->offset[i].y;
offset_[i].w = tf->offset[i].w;
}
// Crea los objetos
sprite_ = std::make_unique<Sprite>(texture, (SDL_FRect){0, 0, static_cast<float>(box_width_), static_cast<float>(box_height_)});
white_sprite_ = std::make_unique<Sprite>(white_texture, (SDL_FRect){0, 0, static_cast<float>(box_width_), static_cast<float>(box_height_)});
// Inicializa variables
fixed_width_ = false;
}
// Constructor con textura blanca opcional
Text::Text(const std::shared_ptr<Texture>& texture, const std::shared_ptr<Texture>& white_texture, const std::shared_ptr<Text::File>& text_file) {
// Inicializa variables desde la estructura
box_height_ = text_file->box_height;
box_width_ = text_file->box_width;
for (int i = 0; i < 128; ++i) {
offset_[i].x = text_file->offset[i].x;
offset_[i].y = text_file->offset[i].y;
offset_[i].w = text_file->offset[i].w;
}
// Crea los objetos
sprite_ = std::make_unique<Sprite>(texture, (SDL_FRect){0, 0, static_cast<float>(box_width_), static_cast<float>(box_height_)});
white_sprite_ = std::make_unique<Sprite>(white_texture, (SDL_FRect){0, 0, static_cast<float>(box_width_), static_cast<float>(box_height_)});
// Inicializa variables
fixed_width_ = false;
}
// Escribe texto en pantalla
void Text::write(int x, int y, const std::string& text, int kerning, int length) {
int shift = 0;
const std::string_view VISIBLE_TEXT = (length == -1) ? std::string_view(text) : std::string_view(text).substr(0, length);
sprite_->setY(y);
for (const auto CH : VISIBLE_TEXT) {
const auto INDEX = static_cast<unsigned char>(CH);
if (INDEX < offset_.size()) {
sprite_->setSpriteClip(offset_[INDEX].x, offset_[INDEX].y, box_width_, box_height_);
sprite_->setX(x + shift);
sprite_->render();
shift += offset_[INDEX].w + kerning;
}
}
}
// Escribe el texto al doble de tamaño
void Text::write2X(int x, int y, const std::string& text, int kerning, int length) {
int shift = 0;
const std::string_view VISIBLE_TEXT = (length == -1) ? std::string_view(text) : std::string_view(text).substr(0, length);
for (const auto CH : VISIBLE_TEXT) {
const auto INDEX = static_cast<unsigned char>(CH);
if (INDEX < offset_.size()) {
SDL_FRect rect = {
static_cast<float>(offset_[INDEX].x),
static_cast<float>(offset_[INDEX].y),
static_cast<float>(box_width_),
static_cast<float>(box_height_)};
sprite_->getTexture()->render(x + shift, y, &rect, 2.0F, 2.0F);
shift += (offset_[INDEX].w + kerning) * 2;
}
}
}
// Escribe el texto en una textura
auto Text::writeToTexture(const std::string& text, int zoom, int kerning, int length) -> std::shared_ptr<Texture> {
auto* renderer = Screen::get()->getRenderer();
auto texture = std::make_shared<Texture>(renderer);
auto width = Text::length(text, kerning) * zoom;
auto height = box_height_ * zoom;
auto* temp = SDL_GetRenderTarget(renderer);
texture->createBlank(width, height, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET);
texture->setBlendMode(SDL_BLENDMODE_BLEND);
texture->setAsRenderTarget(renderer);
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);
SDL_RenderClear(renderer);
zoom == 1 ? write(0, 0, text, kerning) : write2X(0, 0, text, kerning);
SDL_SetRenderTarget(renderer, temp);
return texture;
}
// Escribe el texto con extras en una textura
auto Text::writeDXToTexture(Uint8 flags, const std::string& text, int kerning, Color text_color, Uint8 shadow_distance, Color shadow_color, int length) -> std::shared_ptr<Texture> {
auto* renderer = Screen::get()->getRenderer();
auto texture = std::make_shared<Texture>(renderer);
// Calcula las dimensiones considerando los efectos
auto base_width = Text::length(text, kerning);
auto base_height = box_height_;
auto width = base_width;
auto height = base_height;
auto offset_x = 0;
auto offset_y = 0;
const auto STROKED = ((flags & Text::STROKE) == Text::STROKE);
const auto SHADOWED = ((flags & Text::SHADOW) == Text::SHADOW);
if (STROKED) {
// Para stroke, el texto se expande en todas las direcciones por shadow_distance
width = base_width + (shadow_distance * 2);
height = base_height + (shadow_distance * 2);
offset_x = shadow_distance;
offset_y = shadow_distance;
} else if (SHADOWED) {
// Para shadow, solo se añade espacio a la derecha y abajo
width = base_width + shadow_distance;
height = base_height + shadow_distance;
}
auto* temp = SDL_GetRenderTarget(renderer);
texture->createBlank(width, height, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET);
texture->setBlendMode(SDL_BLENDMODE_BLEND);
texture->setAsRenderTarget(renderer);
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);
SDL_RenderClear(renderer);
writeDX(flags, offset_x, offset_y, text, kerning, text_color, shadow_distance, shadow_color, length);
SDL_SetRenderTarget(renderer, temp);
return texture;
}
// Escribe el texto con colores
void Text::writeColored(int x, int y, const std::string& text, Color color, int kerning, int length) {
writeColoredWithSprite(sprite_.get(), x, y, text, color, kerning, length);
}
// Escribe el texto con colores usando un sprite específico
void Text::writeColoredWithSprite(Sprite* sprite, int x, int y, const std::string& text, Color color, int kerning, int length) {
int shift = 0;
const std::string_view VISIBLE_TEXT = (length == -1) ? std::string_view(text) : std::string_view(text).substr(0, length);
auto* texture = sprite->getTexture().get();
// Guarda el alpha original y aplica el nuevo
Uint8 original_alpha;
SDL_GetTextureAlphaMod(texture->getSDLTexture(), &original_alpha);
texture->setAlpha(color.a);
texture->setColor(color.r, color.g, color.b);
sprite->setY(y);
for (const auto CH : VISIBLE_TEXT) {
const auto INDEX = static_cast<unsigned char>(CH);
if (INDEX < offset_.size()) {
sprite->setSpriteClip(offset_[INDEX].x, offset_[INDEX].y, box_width_, box_height_);
sprite->setX(x + shift);
sprite->render();
shift += offset_[INDEX].w + kerning;
}
}
// Restaura los valores originales
texture->setColor(255, 255, 255);
texture->setAlpha(255);
}
// Escribe stroke con alpha correcto usando textura temporal
void Text::writeStrokeWithAlpha(int x, int y, const std::string& text, int kerning, Color stroke_color, Uint8 shadow_distance, int length) {
auto* renderer = Screen::get()->getRenderer();
auto* original_target = SDL_GetRenderTarget(renderer);
// Calcula dimensiones de la textura temporal
auto text_width = Text::length(text, kerning);
auto text_height = box_height_;
auto temp_width = text_width + (shadow_distance * 2);
auto temp_height = text_height + (shadow_distance * 2);
// Crea textura temporal
auto temp_texture = std::make_shared<Texture>(renderer);
temp_texture->createBlank(temp_width, temp_height, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET);
temp_texture->setBlendMode(SDL_BLENDMODE_BLEND);
// Renderiza el stroke en la textura temporal
temp_texture->setAsRenderTarget(renderer);
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);
SDL_RenderClear(renderer);
// Selecciona el sprite apropiado para el stroke
auto* stroke_sprite = white_sprite_ ? white_sprite_.get() : sprite_.get();
// Renderiza stroke sin alpha (sólido) en textura temporal
Color solid_color = Color(stroke_color.r, stroke_color.g, stroke_color.b, 255);
for (int dist = 1; dist <= shadow_distance; ++dist) {
for (int dy = -dist; dy <= dist; ++dy) {
for (int dx = -dist; dx <= dist; ++dx) {
writeColoredWithSprite(stroke_sprite, shadow_distance + dx, shadow_distance + dy, text, solid_color, kerning, length);
}
}
}
// Restaura render target original
SDL_SetRenderTarget(renderer, original_target);
// Renderiza la textura temporal con el alpha deseado
temp_texture->setAlpha(stroke_color.a);
temp_texture->render(x - shadow_distance, y - shadow_distance);
}
// Escribe el texto con sombra
void Text::writeShadowed(int x, int y, const std::string& text, Color color, Uint8 shadow_distance, int kerning, int length) {
writeDX(Text::SHADOW, x, y, text, kerning, color, shadow_distance, color, length);
write(x, y, text, kerning, length); // Dibuja el texto principal encima
}
// Escribe el texto centrado en un punto x
void Text::writeCentered(int x, int y, const std::string& text, int kerning, int length) {
x -= (Text::length(text, kerning) / 2);
write(x, y, text, kerning, length);
}
// Escribe texto con extras
void Text::writeDX(Uint8 flags, int x, int y, const std::string& text, int kerning, Color text_color, Uint8 shadow_distance, Color shadow_color, int length) {
const auto CENTERED = ((flags & Text::CENTER) == Text::CENTER);
const auto SHADOWED = ((flags & Text::SHADOW) == Text::SHADOW);
const auto COLORED = ((flags & Text::COLOR) == Text::COLOR);
const auto STROKED = ((flags & Text::STROKE) == Text::STROKE);
if (CENTERED) {
x -= (Text::length(text, kerning) / 2);
}
if (SHADOWED) {
if (white_sprite_) {
writeColoredWithSprite(white_sprite_.get(), x + shadow_distance, y + shadow_distance, text, shadow_color, kerning, length);
} else {
writeColored(x + shadow_distance, y + shadow_distance, text, shadow_color, kerning, length);
}
}
if (STROKED) {
if (shadow_color.a < 255) {
// Usa textura temporal para alpha correcto
writeStrokeWithAlpha(x, y, text, kerning, shadow_color, shadow_distance, length);
} else {
// Método tradicional para stroke sólido
for (int dist = 1; dist <= shadow_distance; ++dist) {
for (int dy = -dist; dy <= dist; ++dy) {
for (int dx = -dist; dx <= dist; ++dx) {
if (white_sprite_) {
writeColoredWithSprite(white_sprite_.get(), x + dx, y + dy, text, shadow_color, kerning, length);
} else {
writeColored(x + dx, y + dy, text, shadow_color, kerning, length);
}
}
}
}
}
}
if (COLORED) {
writeColored(x, y, text, text_color, kerning, length);
} else {
write(x, y, text, kerning, length);
}
}
// Escribe texto a partir de un TextStyle
void Text::writeStyle(int x, int y, const std::string& text, const Style& style, int length) {
writeDX(style.flags, x, y, text, style.kerning, style.text_color, style.shadow_distance, style.shadow_color);
}
// Obtiene la longitud en pixels de una cadena
auto Text::length(const std::string& text, int kerning) const -> int {
int shift = 0;
for (const auto& ch : text) {
// Convertimos a unsigned char para obtener el valor ASCII correcto (0-255)
const auto INDEX = static_cast<unsigned char>(ch);
// Verificamos si el carácter está dentro de los límites del array
if (INDEX < offset_.size()) {
shift += (offset_[INDEX].w + kerning);
}
}
// Descuenta el kerning del último caracter si el texto no está vacío
return text.empty() ? 0 : shift - kerning;
}
// Devuelve el valor de la variable
auto Text::getCharacterSize() const -> int {
return box_width_;
}
// Establece si se usa un tamaño fijo de letra
void Text::setFixedWidth(bool value) {
fixed_width_ = value;
}
// Llena una estructuta TextFile desde un fichero
auto Text::loadFile(const std::string& file_path) -> std::shared_ptr<Text::File> {
auto tf = std::make_shared<Text::File>();
// Inicializa a cero el vector con las coordenadas
for (auto& i : tf->offset) {
i.x = 0;
i.y = 0;
i.w = 0;
tf->box_width = 0;
tf->box_height = 0;
}
// Intenta cargar desde ResourceHelper primero
auto resource_data = ResourceHelper::loadFile(file_path);
std::istringstream stream;
bool using_resource_data = false;
if (!resource_data.empty()) {
std::string content(resource_data.begin(), resource_data.end());
stream.str(content);
using_resource_data = true;
}
// Fallback a archivo directo
std::ifstream file;
if (!using_resource_data) {
file.open(file_path);
}
std::istream& input_stream = using_resource_data ? stream : static_cast<std::istream&>(file);
if ((using_resource_data && stream.good()) || (!using_resource_data && file.is_open() && file.good())) {
std::string buffer;
// Lee los dos primeros valores del fichero
std::getline(input_stream, buffer);
std::getline(input_stream, buffer);
tf->box_width = std::stoi(buffer);
std::getline(input_stream, buffer);
std::getline(input_stream, buffer);
tf->box_height = std::stoi(buffer);
// lee el resto de datos del fichero
auto index = 32;
auto line_read = 0;
while (std::getline(input_stream, buffer)) {
// Almacena solo las lineas impares
if (line_read % 2 == 1) {
tf->offset[index++].w = std::stoi(buffer);
}
// Limpia el buffer
buffer.clear();
line_read++;
};
Logger::dots("Text File : ", getFileName(file_path), "[ LOADED ]");
// Cierra el fichero si se usó
if (!using_resource_data && file.is_open()) {
file.close();
}
}
// El fichero no se puede abrir
else {
std::cerr << "Error: Fichero no encontrado " << getFileName(file_path) << '\n';
throw std::runtime_error("Fichero no encontrado: " + getFileName(file_path));
}
// Establece las coordenadas para cada caracter ascii de la cadena y su ancho
for (int i = 32; i < 128; ++i) {
tf->offset[i].x = ((i - 32) % 15) * tf->box_width;
tf->offset[i].y = ((i - 32) / 15) * tf->box_height;
}
return tf;
}