diff --git a/CLAUDE.md b/CLAUDE.md index f3d7d24..328430e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -**Aventures En Egipte (AEE)** — a retro-style 2D game written in C++ using SDL3. The game uses a software-rendered 8-bit paletted graphics engine (320x200, 256 colors) with an OpenGL CRT shader pass, custom audio (JailAudio replacing SDL_Mixer), and GIF-based assets. The codebase and commit messages are in Valencian/Catalan. +**Aventures En Egipte (AEE)** — a retro-style 2D game written in C++ using SDL3. The game uses a software-rendered 8-bit paletted graphics engine (320x200, 256 colors), custom audio (JailAudio), and GIF-based assets. The codebase and commit messages are in Valencian/Catalan. ## Build @@ -18,38 +18,61 @@ cmake -B build -G "MinGW Makefiles" cmake --build build ``` -Dependencies: SDL3, OpenGL. Uses CMake (minimum 3.10) with C++20. +Dependencies: SDL3. Uses CMake (minimum 3.10) with C++20. -The executable is output to the project root. The `data/` folder contains runtime assets (GIF images, OGG music, GLSL shader) and must be in the working directory at runtime. +The executable is output to the project root. The `data/` folder must be in the working directory at runtime. ## Architecture -### Custom "Jail" Engine Libraries (prefix: J) +### Boundary: Original vs New Code -All engine modules are flat C-style APIs (no classes), prefixed by subsystem: +The codebase has a clear separation between the original game and the new modernization layer: -- **JG** (`source/core/jgame`) — Game loop timing: init/finalize, fixed-timestep update via `JG_ShouldUpdate()` -- **JD8** (`source/core/jdraw8`) — 8-bit paletted software renderer. 320x200 screen buffer (`JD8_Surface` = `Uint8*`), palette-indexed blitting with color-key transparency, fade effects. `JD8_Flip()` converts the indexed buffer to ARGB, uploads to SDL texture, and renders through the CRT shader -- **JA** (`source/core/jail_audio`) — Custom audio mixing using SDL3 audio streams directly (OGG via stb_vorbis, WAV). Manages music and sound channels independently -- **JI** (`source/core/jinput`) — Input: keyboard state polling, key debouncing, cheat code detection -- **JF** (`source/core/jfile`) — File I/O: supports loading from filesystem folder or a packed resource file (`.jrf`). Currently uses folder mode (`data/`) -- **shader** (`source/core/jshader`) — OpenGL post-processing shader (CRT effect) applied to the back buffer +| Path | Owner | Rule | +|------|-------|------| +| `source/core/jail/` | Original engine | **Do not modify** gameplay behavior | +| `source/game/*.cpp/hpp` (except options/defines/defaults) | Original game | **Do not modify** | +| `source/core/rendering/` | New presentation layer | Free to modify | +| `source/core/input/` | New input layer | Free to modify | +| `source/game/options.hpp/cpp` | New config system | Free to modify | +| `source/game/defines.hpp` | New constants | Free to modify | +| `source/game/defaults.hpp` | New defaults | Free to modify | +| `data/*.gif, *.ogg` | Original assets | **Do not modify** | +| `data/fonts/, data/ui/` | New assets | Free to modify | + +### Original "Jail" Engine (`source/core/jail/`) + +Flat C-style APIs (no classes), prefixed by subsystem. **Do not touch gameplay logic.** + +- **JG** (`jgame`) — Game loop timing: init/finalize, fixed-timestep update via `JG_ShouldUpdate()` +- **JD8** (`jdraw8`) — 8-bit paletted software renderer. 320x200 screen buffer (`JD8_Surface` = `Uint8*`), palette-indexed blitting with color-key transparency, fade effects. `JD8_Flip()` converts palette→ARGB and delegates to `Screen::present()` +- **JA** (`jail_audio`) — Custom audio mixing using SDL3 audio streams (OGG via stb_vorbis, WAV) +- **JI** (`jinput`) — Input: keyboard state polling, key debouncing, cheat code detection. Calls `GlobalInputs::handle()` at end of each update +- **JF** (`jfile`) — File I/O: filesystem folder mode (`data/`) or packed resource file (`.jrf`) + +### Presentation Layer (`source/core/rendering/`) + +- **Screen** — Singleton managing SDL_Window, SDL_Renderer, SDL_Texture. Receives ARGB pixel buffer from JD8 and presents it. Handles fullscreen toggle, zoom. Prepared for future SDL3GPU backend + +### Input Layer (`source/core/input/`) + +- **GlobalInputs** — Maps configurable function keys to window management actions (F1 zoom-, F2 zoom+, F3 fullscreen). Reads key bindings from `Options::keys_gui` ### Configuration System (`source/game/`) Follows the pattern from `jaildoctors_dilemma`: -- **defines.hpp** �� Game-wide constants (window title, version, screen dimensions) -- **defaults.hpp** — Default values for all persistent options (audio, game) +- **defines.hpp** — Game-wide constants (window title, version, screen dimensions) +- **defaults.hpp** — Default values for all persistent options, including key bindings (`Defaults::KeysGUI`, `Defaults::KeysGame`) - **options.hpp/cpp** — `Options` namespace with structs, inline globals, and YAML load/save API. Config persists to `~/.config/jailgames/aee/config.yaml` (Linux), `%APPDATA%/jailgames/aee/` (Windows) -### Game Modules (`source/game/`) +### Game Modules (`source/game/`) — Original, Do Not Touch -- **ModuleSequence** — Non-gameplay screens: intro, menu, slides, banners, credits, death screen. State machine entry point (state=1) -- **ModuleGame** — Core gameplay loop. Owns and orchestrates all game objects. State=0 +- **ModuleSequence** — Non-gameplay screens: intro, menu, slides, banners, credits, death screen (state=1) +- **ModuleGame** — Core gameplay loop, orchestrates all game objects (state=0) - **Sprite** — Base class for animated entities (frame/animation data via `Entitat`) - **Prota** — Player character ("Sam"), extends Sprite -- **Mapa** — Level map with tomb grid (16 tombs), items (treasure, keys, pharaoh, mummy, scroll, diamond), door logic +- **Mapa** — Level map with tomb grid (16 tombs), items, door logic - **Momia** — Enemy: mummies - **Bola** — Enemy: projectile ball - **Marcador** — HUD/scoreboard @@ -61,19 +84,21 @@ Follows the pattern from `jaildoctors_dilemma`: - `stb_vorbis.h` — stb single-header OGG decoder - `fkyaml_node.hpp` — Header-only YAML parser (fkYAML v0.4.2) +### Data Assets (`data/`) + +- `*.gif`, `*.ogg`, `crtpi.glsl` — Original game assets (**do not modify**) +- `fonts/` — New font assets for overlay/UI +- `ui/` — New UI graphics for overlay + ### Main Loop (`source/main.cpp`) A state machine alternates between `ModuleSequence` (state 1) and `ModuleGame` (state 0). Each module's `Go()` returns the next state (-1 to quit). Modules are allocated/freed each transition. -### Golden Rule: Do Not Touch Gameplay - -The original game logic (gameplay, entities, map, scoring, collisions, animations) must remain untouched. All modernization work targets the presentation layer and infrastructure only: window management, configuration/persistence, rendering pipeline (overlay/UI), and build system. Any new feature (save states, options menu, etc.) must be implemented as an overlay on top of the existing game, never by modifying the original gameplay code. - ### Key Conventions - All surfaces are 320x200 = 64000 bytes. Pixel coordinates assume this fixed resolution - Graphics loaded from GIF files, palettes extracted from GIF headers - Music files are numbered OGG files (`00000001.ogg` etc.) - `trick.ini` presence enables the secret character -- Includes use absolute paths from `source/` (e.g., `#include "core/jgame.hpp"`, `#include "game/info.hpp"`) +- Includes use absolute paths from `source/` (e.g., `#include "core/jail/jgame.hpp"`, `#include "game/info.hpp"`) - Headers use `.hpp` extension; external third-party headers in `source/external/` keep `.h` diff --git a/CMakeLists.txt b/CMakeLists.txt index 543f5a4..fc0593f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,14 +12,20 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # --- LISTA EXPLÍCITA DE FUENTES --- set(APP_SOURCES - # Core - Motor "Jail" - source/core/global_inputs.cpp - source/core/jail_audio.cpp - source/core/jdraw8.cpp - source/core/jfile.cpp - source/core/jgame.cpp - source/core/jinput.cpp - source/core/screen.cpp + # Core - Motor original "Jail" (no tocar gameplay) + source/core/jail/jail_audio.cpp + source/core/jail/jdraw8.cpp + source/core/jail/jfile.cpp + source/core/jail/jgame.cpp + source/core/jail/jinput.cpp + + # Core - Capa de presentación (nueva) + source/core/rendering/overlay.cpp + source/core/rendering/screen.cpp + source/core/rendering/text.cpp + + # Core - Input global (nueva) + source/core/input/global_inputs.cpp # Game source/game/options.cpp diff --git a/data/fonts/.gitkeep b/data/fonts/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/data/fonts/8bithud.fnt b/data/fonts/8bithud.fnt new file mode 100644 index 0000000..5cdf2e8 --- /dev/null +++ b/data/fonts/8bithud.fnt @@ -0,0 +1,132 @@ +# Font: 8bithud — generado desde 8-bit-hud.ttf size 5 +# Generado con tools/font_gen/font_gen.py + +box_width 8 +box_height 8 +columns 15 + +# codepoint_decimal ancho_visual +32 3 # U+0020 +33 2 # ! +34 5 # " +35 6 # # +36 6 # $ +37 6 # % +38 6 # & +39 2 # ' +40 3 # ( +41 3 # ) +42 4 # * +43 3 # + +44 2 # , +45 3 # - +46 2 # . +47 4 # / +48 6 # 0 +49 3 # 1 +50 6 # 2 +51 6 # 3 +52 6 # 4 +53 6 # 5 +54 6 # 6 +55 6 # 7 +56 6 # 8 +57 6 # 9 +58 2 # : +59 2 # ; +60 4 # < +61 3 # = +62 4 # > +63 6 # ? +64 8 # @ +65 6 # A +66 6 # B +67 6 # C +68 6 # D +69 6 # E +70 6 # F +71 6 # G +72 6 # H +73 6 # I +74 6 # J +75 6 # K +76 6 # L +77 6 # M +78 6 # N +79 6 # O +80 6 # P +81 6 # Q +82 6 # R +83 6 # S +84 6 # T +85 6 # U +86 5 # V +87 6 # W +88 6 # X +89 6 # Y +90 6 # Z +91 3 # [ +92 4 # \ +93 3 # ] +94 4 # ^ +95 6 # _ +96 2 # ` +97 5 # a +98 5 # b +99 5 # c +100 5 # d +101 5 # e +102 5 # f +103 5 # g +104 5 # h +105 4 # i +106 5 # j +107 5 # k +108 5 # l +109 6 # m +110 5 # n +111 5 # o +112 5 # p +113 5 # q +114 5 # r +115 5 # s +116 4 # t +117 5 # u +118 5 # v +119 6 # w +120 4 # x +121 4 # y +122 5 # z +123 4 # { +124 1 # | +125 4 # } +126 4 # ~ +192 6 # À +193 6 # Á +200 6 # È +201 6 # É +204 6 # Ì +205 6 # Í +210 6 # Ò +211 6 # Ó +219 6 # Ù +218 6 # Ú +209 6 # Ñ +199 6 # Ç +224 5 # à +225 5 # á +232 5 # è +233 5 # é +236 4 # ì +237 4 # í +242 5 # ò +243 5 # ó +249 5 # ù +250 5 # ú +241 5 # ñ +231 5 # ç +161 2 # ¡ +191 6 # ¿ +171 4 # « +187 4 # » +183 2 # · diff --git a/data/fonts/8bithud.gif b/data/fonts/8bithud.gif new file mode 100644 index 0000000..85a6a8b Binary files /dev/null and b/data/fonts/8bithud.gif differ diff --git a/data/ui/.gitkeep b/data/ui/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/source/core/global_inputs.cpp b/source/core/global_inputs.cpp deleted file mode 100644 index eb14841..0000000 --- a/source/core/global_inputs.cpp +++ /dev/null @@ -1,37 +0,0 @@ -#include "core/global_inputs.hpp" - -#include - -#include "core/jinput.hpp" -#include "core/screen.hpp" - -namespace GlobalInputs { - - static bool f1_was_pressed = false; - static bool f2_was_pressed = false; - static bool f3_was_pressed = false; - - void handle() { - // F1 — decrement zoom - bool f1 = JI_KeyPressed(SDL_SCANCODE_F1); - if (f1 && !f1_was_pressed) { - Screen::get()->decZoom(); - } - f1_was_pressed = f1; - - // F2 — increment zoom - bool f2 = JI_KeyPressed(SDL_SCANCODE_F2); - if (f2 && !f2_was_pressed) { - Screen::get()->incZoom(); - } - f2_was_pressed = f2; - - // F3 — toggle fullscreen - bool f3 = JI_KeyPressed(SDL_SCANCODE_F3); - if (f3 && !f3_was_pressed) { - Screen::get()->toggleFullscreen(); - } - f3_was_pressed = f3; - } - -} // namespace GlobalInputs diff --git a/source/core/input/global_inputs.cpp b/source/core/input/global_inputs.cpp new file mode 100644 index 0000000..01e1405 --- /dev/null +++ b/source/core/input/global_inputs.cpp @@ -0,0 +1,47 @@ +#include "core/input/global_inputs.hpp" + +#include +#include + +#include "core/jail/jinput.hpp" +#include "core/rendering/overlay.hpp" +#include "core/rendering/screen.hpp" +#include "game/options.hpp" + +namespace GlobalInputs { + + static bool dec_zoom_was_pressed = false; + static bool inc_zoom_was_pressed = false; + static bool fullscreen_was_pressed = false; + + void handle() { + // Decrement zoom + bool dec_zoom = JI_KeyPressed(Options::keys_gui.dec_zoom); + if (dec_zoom && !dec_zoom_was_pressed) { + Screen::get()->decZoom(); + char msg[32]; + snprintf(msg, sizeof(msg), "ZOOM %dx", Screen::get()->getZoom()); + Overlay::showNotification(msg); + } + dec_zoom_was_pressed = dec_zoom; + + // Increment zoom + bool inc_zoom = JI_KeyPressed(Options::keys_gui.inc_zoom); + if (inc_zoom && !inc_zoom_was_pressed) { + Screen::get()->incZoom(); + char msg[32]; + snprintf(msg, sizeof(msg), "ZOOM %dx", Screen::get()->getZoom()); + Overlay::showNotification(msg); + } + inc_zoom_was_pressed = inc_zoom; + + // Toggle fullscreen + bool fullscreen = JI_KeyPressed(Options::keys_gui.fullscreen); + if (fullscreen && !fullscreen_was_pressed) { + Screen::get()->toggleFullscreen(); + Overlay::showNotification(Screen::get()->isFullscreen() ? "FULLSCREEN" : "WINDOWED"); + } + fullscreen_was_pressed = fullscreen; + } + +} // namespace GlobalInputs diff --git a/source/core/global_inputs.hpp b/source/core/input/global_inputs.hpp similarity index 100% rename from source/core/global_inputs.hpp rename to source/core/input/global_inputs.hpp diff --git a/source/core/jail_audio.cpp b/source/core/jail/jail_audio.cpp similarity index 99% rename from source/core/jail_audio.cpp rename to source/core/jail/jail_audio.cpp index 092ea7f..cb9327c 100644 --- a/source/core/jail_audio.cpp +++ b/source/core/jail/jail_audio.cpp @@ -1,5 +1,5 @@ #ifndef JA_USESDLMIXER -#include "core/jail_audio.hpp" +#include "core/jail/jail_audio.hpp" #include #include diff --git a/source/core/jail_audio.hpp b/source/core/jail/jail_audio.hpp similarity index 100% rename from source/core/jail_audio.hpp rename to source/core/jail/jail_audio.hpp diff --git a/source/core/jdraw8.cpp b/source/core/jail/jdraw8.cpp similarity index 95% rename from source/core/jdraw8.cpp rename to source/core/jail/jdraw8.cpp index ad363fc..e5e3fae 100644 --- a/source/core/jdraw8.cpp +++ b/source/core/jail/jdraw8.cpp @@ -1,9 +1,9 @@ -#include "core/jdraw8.hpp" +#include "core/jail/jdraw8.hpp" #include -#include "core/jfile.hpp" -#include "core/screen.hpp" +#include "core/jail/jfile.hpp" +#include "core/rendering/screen.hpp" #include "external/gif.h" JD8_Surface screen = NULL; diff --git a/source/core/jdraw8.hpp b/source/core/jail/jdraw8.hpp similarity index 100% rename from source/core/jdraw8.hpp rename to source/core/jail/jdraw8.hpp diff --git a/source/core/jfile.cpp b/source/core/jail/jfile.cpp similarity index 99% rename from source/core/jfile.cpp rename to source/core/jail/jfile.cpp index 66c00bf..0ec7d94 100644 --- a/source/core/jfile.cpp +++ b/source/core/jail/jfile.cpp @@ -1,4 +1,4 @@ -#include "core/jfile.hpp" +#include "core/jail/jfile.hpp" #include #include diff --git a/source/core/jfile.hpp b/source/core/jail/jfile.hpp similarity index 100% rename from source/core/jfile.hpp rename to source/core/jail/jfile.hpp diff --git a/source/core/jgame.cpp b/source/core/jail/jgame.cpp similarity index 90% rename from source/core/jgame.cpp rename to source/core/jail/jgame.cpp index bb7863e..1b17515 100644 --- a/source/core/jgame.cpp +++ b/source/core/jail/jgame.cpp @@ -1,4 +1,4 @@ -#include "core/jgame.hpp" +#include "core/jail/jgame.hpp" bool eixir = false; Uint32 updateTicks = 0; diff --git a/source/core/jgame.hpp b/source/core/jail/jgame.hpp similarity index 100% rename from source/core/jgame.hpp rename to source/core/jail/jgame.hpp diff --git a/source/core/jinput.cpp b/source/core/jail/jinput.cpp similarity index 87% rename from source/core/jinput.cpp rename to source/core/jail/jinput.cpp index 04528f7..9ff9be3 100644 --- a/source/core/jinput.cpp +++ b/source/core/jail/jinput.cpp @@ -1,9 +1,9 @@ -#include "core/jinput.hpp" +#include "core/jail/jinput.hpp" #include -#include "core/global_inputs.hpp" -#include "core/jgame.hpp" +#include "core/input/global_inputs.hpp" +#include "core/jail/jgame.hpp" const bool* keystates; // = SDL_GetKeyboardState( NULL ); SDL_Event event; diff --git a/source/core/jinput.hpp b/source/core/jail/jinput.hpp similarity index 100% rename from source/core/jinput.hpp rename to source/core/jail/jinput.hpp diff --git a/source/core/rendering/overlay.cpp b/source/core/rendering/overlay.cpp new file mode 100644 index 0000000..e3c7a1b --- /dev/null +++ b/source/core/rendering/overlay.cpp @@ -0,0 +1,119 @@ +#include "core/rendering/overlay.hpp" + +#include +#include +#include +#include + +#include "core/rendering/text.hpp" + +namespace Overlay { + + static std::unique_ptr font_; + + // --- Notificacions amb animació --- + + enum class Status { RISING, STAY, VANISHING, FINISHED }; + + struct Notification { + std::string message; + Status status{Status::RISING}; + float y_offset{0.0F}; // 0 = fora de pantalla, 1 = posició final + float timer{0.0F}; + float duration{2.0F}; + }; + + static std::vector notifications_; + static Uint32 last_ticks_ = 0; + + static constexpr float SLIDE_SPEED = 4.0F; // velocitat d'animació (unitats/segon) + static constexpr int BAR_HEIGHT = 12; // alçada de la barra + static constexpr int TEXT_Y_OFFSET = 2; // offset del text dins la barra + static constexpr Uint32 BG_COLOR = 0xFF1A1A2E; // fons blau fosc + static constexpr Uint32 TEXT_COLOR = 0xFF00FFFF; // cyan + static constexpr int SCREEN_W = 320; + static constexpr int SCREEN_H = 200; + + void init() { + font_ = std::make_unique("fonts/8bithud.fnt", "fonts/8bithud.gif"); + last_ticks_ = SDL_GetTicks(); + } + + void destroy() { + font_.reset(); + notifications_.clear(); + } + + static void drawBar(Uint32* pixel_data, int y, int h, Uint32 color) { + for (int row = y; row < y + h; row++) { + if (row < 0 || row >= SCREEN_H) continue; + for (int col = 0; col < SCREEN_W; col++) { + pixel_data[col + row * SCREEN_W] = color; + } + } + } + + void render(Uint32* pixel_data) { + if (!font_ || !pixel_data) return; + + // Calcula delta time + Uint32 now = SDL_GetTicks(); + float dt = static_cast(now - last_ticks_) / 1000.0F; + last_ticks_ = now; + + // Actualitza i pinta cada notificació + for (auto& notif : notifications_) { + switch (notif.status) { + case Status::RISING: + notif.y_offset += SLIDE_SPEED * dt; + if (notif.y_offset >= 1.0F) { + notif.y_offset = 1.0F; + notif.status = Status::STAY; + notif.timer = 0.0F; + } + break; + + case Status::STAY: + notif.timer += dt; + if (notif.timer >= notif.duration) { + notif.status = Status::VANISHING; + } + break; + + case Status::VANISHING: + notif.y_offset -= SLIDE_SPEED * dt; + if (notif.y_offset <= 0.0F) { + notif.status = Status::FINISHED; + } + break; + + case Status::FINISHED: + break; + } + + if (notif.status == Status::FINISHED) continue; + + // Posició: puja des de sota la pantalla + int bar_y = SCREEN_H - static_cast(notif.y_offset * BAR_HEIGHT); + + // Pinta fons + drawBar(pixel_data, bar_y, BAR_HEIGHT, BG_COLOR); + + // Pinta text centrat + font_->drawCentered(pixel_data, bar_y + TEXT_Y_OFFSET, notif.message.c_str(), TEXT_COLOR); + } + + // Elimina les acabades + notifications_.erase( + std::remove_if(notifications_.begin(), notifications_.end(), + [](const Notification& n) { return n.status == Status::FINISHED; }), + notifications_.end()); + } + + void showNotification(const char* text, float duration_seconds) { + // Si ja hi ha una notificació, la reemplaça (no apilem) + notifications_.clear(); + notifications_.push_back({text, Status::RISING, 0.0F, 0.0F, duration_seconds}); + } + +} // namespace Overlay diff --git a/source/core/rendering/overlay.hpp b/source/core/rendering/overlay.hpp new file mode 100644 index 0000000..225e800 --- /dev/null +++ b/source/core/rendering/overlay.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include + +namespace Overlay { + void init(); + void destroy(); + + // Pinta l'overlay sobre el buffer ARGB — cridar abans de presentar + void render(Uint32* pixel_data); + + // Mostra una notificació amb animació slide-in/stay/slide-out + void showNotification(const char* text, float duration_seconds = 2.0F); +} // namespace Overlay diff --git a/source/core/screen.cpp b/source/core/rendering/screen.cpp similarity index 95% rename from source/core/screen.cpp rename to source/core/rendering/screen.cpp index 6765439..b9b42bf 100644 --- a/source/core/screen.cpp +++ b/source/core/rendering/screen.cpp @@ -1,7 +1,8 @@ -#include "core/screen.hpp" +#include "core/rendering/screen.hpp" #include +#include "core/rendering/overlay.hpp" #include "game/defines.hpp" #include "game/options.hpp" @@ -53,7 +54,8 @@ Screen::~Screen() { if (window_) SDL_DestroyWindow(window_); } -void Screen::present(const Uint32* pixel_data) { +void Screen::present(Uint32* pixel_data) { + Overlay::render(pixel_data); SDL_UpdateTexture(texture_, nullptr, pixel_data, GAME_WIDTH * sizeof(Uint32)); SDL_RenderClear(renderer_); SDL_RenderTexture(renderer_, texture_, nullptr, nullptr); diff --git a/source/core/screen.hpp b/source/core/rendering/screen.hpp similarity index 96% rename from source/core/screen.hpp rename to source/core/rendering/screen.hpp index 3c886ba..39b57d2 100644 --- a/source/core/screen.hpp +++ b/source/core/rendering/screen.hpp @@ -11,7 +11,7 @@ class Screen { static auto get() -> Screen*; // Presentació — rep el buffer ARGB de 320x200 de JD8 - void present(const Uint32* pixel_data); + void present(Uint32* pixel_data); // Gestió de finestra void toggleFullscreen(); diff --git a/source/core/rendering/text.cpp b/source/core/rendering/text.cpp new file mode 100644 index 0000000..3826935 --- /dev/null +++ b/source/core/rendering/text.cpp @@ -0,0 +1,239 @@ +#include "core/rendering/text.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include "core/jail/jfile.hpp" + +// Forward declarations de gif.h (inclòs des de jdraw8.cpp, no es pot incloure dos vegades) +struct rgb; +extern unsigned char* LoadGif(unsigned char* data, unsigned short* w, unsigned short* h); + +Text::Text(const char* fnt_file, const char* gif_file) { + loadBitmap(gif_file); + loadFont(fnt_file); +} + +Text::~Text() { + if (bitmap_) free(bitmap_); +} + +// --- UTF-8 --- + +auto Text::nextCodepoint(const char*& ptr) -> uint32_t { + auto byte = static_cast(*ptr); + if (byte == 0) return 0; + + uint32_t cp = 0; + int extra = 0; + + if (byte < 0x80) { + cp = byte; + } else if ((byte & 0xE0) == 0xC0) { + cp = byte & 0x1F; + extra = 1; + } else if ((byte & 0xF0) == 0xE0) { + cp = byte & 0x0F; + extra = 2; + } else if ((byte & 0xF8) == 0xF0) { + cp = byte & 0x07; + extra = 3; + } else { + ptr++; + return 0xFFFD; + } + + ptr++; + for (int i = 0; i < extra; i++) { + auto cont = static_cast(*ptr); + if ((cont & 0xC0) != 0x80) return 0xFFFD; + cp = (cp << 6) | (cont & 0x3F); + ptr++; + } + + return cp; +} + +// --- Càrrega de font --- + +void Text::loadFont(const char* fnt_file) { + int filesize = 0; + char* buffer = file_getfilebuffer(fnt_file, filesize, true); + if (!buffer) { + std::cerr << "Text: unable to load font file: " << fnt_file << '\n'; + return; + } + + std::istringstream stream(std::string(buffer, filesize)); + free(buffer); + + std::string line; + int glyph_index = 0; + + while (std::getline(stream, line)) { + // Ignora comentaris i línies buides + if (line.empty() || line[0] == '#') continue; + + // Elimina comentaris inline + auto comment_pos = line.find('#'); + if (comment_pos != std::string::npos) { + line = line.substr(0, comment_pos); + } + + // Parseja directives + if (line.find("box_width") == 0) { + sscanf(line.c_str(), "box_width %d", &box_width_); + continue; + } + if (line.find("box_height") == 0) { + sscanf(line.c_str(), "box_height %d", &box_height_); + continue; + } + if (line.find("columns") == 0) { + sscanf(line.c_str(), "columns %d", &columns_); + continue; + } + if (line.find("cell_spacing") == 0) { + sscanf(line.c_str(), "cell_spacing %d", &cell_spacing_); + continue; + } + if (line.find("row_spacing") == 0) { + sscanf(line.c_str(), "row_spacing %d", &row_spacing_); + continue; + } + + // Línies de glifos: "codepoint width" + uint32_t codepoint = 0; + int visual_width = 0; + if (sscanf(line.c_str(), "%u %d", &codepoint, &visual_width) == 2) { + int col = glyph_index % columns_; + int row = glyph_index / columns_; + + GlyphInfo glyph{}; + glyph.x = col * (box_width_ + cell_spacing_) + cell_spacing_; + glyph.y = row * (box_height_ + (row_spacing_ > 0 ? row_spacing_ : cell_spacing_)) + cell_spacing_; + glyph.w = visual_width; + + glyphs_[codepoint] = glyph; + glyph_index++; + } + } + + std::cout << "Text: loaded font with " << glyphs_.size() << " glyphs (" << box_width_ << "x" << box_height_ << ")\n"; +} + +void Text::loadBitmap(const char* gif_file) { + int filesize = 0; + char* buffer = file_getfilebuffer(gif_file, filesize); + if (!buffer) { + std::cerr << "Text: unable to load bitmap: " << gif_file << '\n'; + return; + } + + // Extrau dimensions del header GIF (bytes 6-7 = width, 8-9 = height, little-endian) + auto* raw = reinterpret_cast(buffer); + int w = raw[6] | (raw[7] << 8); + int h = raw[8] | (raw[9] << 8); + + unsigned short gw = 0, gh = 0; + Uint8* pixels = LoadGif(raw, &gw, &gh); + if (!pixels) { + std::cerr << "Text: unable to decode GIF: " << gif_file << '\n'; + free(buffer); + return; + } + + bitmap_width_ = w; + bitmap_height_ = h; + bitmap_ = pixels; + + free(buffer); + std::cout << "Text: bitmap loaded " << w << "x" << h << '\n'; +} + +// --- Renderitzat --- + +void Text::draw(Uint32* pixel_data, int x, int y, const char* text, Uint32 color) const { + if (!bitmap_ || !pixel_data) return; + + const char* ptr = text; + int cursor_x = x; + + while (*ptr) { + uint32_t cp = nextCodepoint(ptr); + if (cp == 0) break; + + auto it = glyphs_.find(cp); + if (it == glyphs_.end()) { + it = glyphs_.find('?'); + if (it == glyphs_.end()) { + cursor_x += box_width_; + continue; + } + } + + const auto& glyph = it->second; + + // Pinta glifo pixel a pixel + for (int gy = 0; gy < box_height_; gy++) { + int dst_y = y + gy; + if (dst_y < 0 || dst_y >= SCREEN_HEIGHT) continue; + + for (int gx = 0; gx < glyph.w; gx++) { + int dst_x = cursor_x + gx; + if (dst_x < 0 || dst_x >= SCREEN_WIDTH) continue; + + int src_x = glyph.x + gx; + int src_y = glyph.y + gy; + + if (src_x >= bitmap_width_ || src_y >= bitmap_height_) continue; + + Uint8 pixel = bitmap_[src_x + src_y * bitmap_width_]; + // Píxel no transparent (índex 0 és fons típicament) + if (pixel != 0) { + pixel_data[dst_x + dst_y * SCREEN_WIDTH] = color; + } + } + } + + cursor_x += glyph.w + 1; // +1 kerning + } +} + +void Text::drawCentered(Uint32* pixel_data, int y, const char* text, Uint32 color) const { + int w = width(text); + int x = (SCREEN_WIDTH - w) / 2; + draw(pixel_data, x, y, text, color); +} + +auto Text::width(const char* text) const -> int { + const char* ptr = text; + int w = 0; + bool first = true; + + while (*ptr) { + uint32_t cp = nextCodepoint(ptr); + if (cp == 0) break; + + auto it = glyphs_.find(cp); + if (it == glyphs_.end()) { + it = glyphs_.find('?'); + } + + if (!first) w += 1; // kerning + first = false; + + if (it != glyphs_.end()) { + w += it->second.w; + } else { + w += box_width_; + } + } + + return w; +} diff --git a/source/core/rendering/text.hpp b/source/core/rendering/text.hpp new file mode 100644 index 0000000..8cd5007 --- /dev/null +++ b/source/core/rendering/text.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include + +#include +#include +#include + +class Text { + public: + Text(const char* fnt_file, const char* gif_file); + ~Text(); + + // Pinta texto sobre un buffer ARGB de 320x200 + void draw(Uint32* pixel_data, int x, int y, const char* text, Uint32 color) const; + void drawCentered(Uint32* pixel_data, int y, const char* text, Uint32 color) const; + + // Calcula ancho en píxeles d'un text + [[nodiscard]] auto width(const char* text) const -> int; + [[nodiscard]] auto charHeight() const -> int { return box_height_; } + + private: + struct GlyphInfo { + int x, y; // posició en el bitmap + int w; // ample visual + }; + + int box_width_{0}; + int box_height_{0}; + int columns_{0}; + int cell_spacing_{0}; + int row_spacing_{0}; + + Uint8* bitmap_{nullptr}; // píxels 8-bit del GIF de la font + int bitmap_width_{0}; + int bitmap_height_{0}; + + std::unordered_map glyphs_; + + static auto nextCodepoint(const char*& ptr) -> uint32_t; + + void loadFont(const char* fnt_file); + void loadBitmap(const char* gif_file); + + static constexpr int SCREEN_WIDTH = 320; + static constexpr int SCREEN_HEIGHT = 200; +}; diff --git a/source/game/bola.cpp b/source/game/bola.cpp index 222577f..332d911 100644 --- a/source/game/bola.cpp +++ b/source/game/bola.cpp @@ -2,7 +2,7 @@ #include -#include "core/jgame.hpp" +#include "core/jail/jgame.hpp" Bola::Bola(JD8_Surface gfx, Prota* sam) : Sprite(gfx) { diff --git a/source/game/defaults.hpp b/source/game/defaults.hpp index d013f9c..8155793 100644 --- a/source/game/defaults.hpp +++ b/source/game/defaults.hpp @@ -1,5 +1,23 @@ #pragma once +#include + +// Tecles GUI (capa de presentació — finestra, zoom, etc.) +namespace Defaults::KeysGUI { + constexpr SDL_Scancode DEC_ZOOM = SDL_SCANCODE_F1; + constexpr SDL_Scancode INC_ZOOM = SDL_SCANCODE_F2; + constexpr SDL_Scancode FULLSCREEN = SDL_SCANCODE_F3; +} // namespace Defaults::KeysGUI + +// Tecles de joc (moviment del personatge, accions) +namespace Defaults::KeysGame { + constexpr SDL_Scancode UP = SDL_SCANCODE_UP; + constexpr SDL_Scancode DOWN = SDL_SCANCODE_DOWN; + constexpr SDL_Scancode LEFT = SDL_SCANCODE_LEFT; + constexpr SDL_Scancode RIGHT = SDL_SCANCODE_RIGHT; + constexpr SDL_Scancode EXIT = SDL_SCANCODE_ESCAPE; +} // namespace Defaults::KeysGame + namespace Defaults::Audio { constexpr float VOLUME = 1.0F; constexpr bool MUSIC_ENABLED = true; diff --git a/source/game/engendro.cpp b/source/game/engendro.cpp index 3c609a8..236d49b 100644 --- a/source/game/engendro.cpp +++ b/source/game/engendro.cpp @@ -2,7 +2,7 @@ #include -#include "core/jgame.hpp" +#include "core/jail/jgame.hpp" Engendro::Engendro(JD8_Surface gfx, Uint16 x, Uint16 y) : Sprite(gfx) { diff --git a/source/game/mapa.cpp b/source/game/mapa.cpp index c7ed57f..6cdb9eb 100644 --- a/source/game/mapa.cpp +++ b/source/game/mapa.cpp @@ -2,8 +2,8 @@ #include -#include "core/jgame.hpp" -#include "core/jinput.hpp" +#include "core/jail/jgame.hpp" +#include "core/jail/jinput.hpp" Mapa::Mapa(JD8_Surface gfx, Prota* sam) { this->gfx = gfx; diff --git a/source/game/mapa.hpp b/source/game/mapa.hpp index cb21791..5584d5e 100644 --- a/source/game/mapa.hpp +++ b/source/game/mapa.hpp @@ -1,6 +1,6 @@ #pragma once -#include "core/jdraw8.hpp" +#include "core/jail/jdraw8.hpp" #include "game/info.hpp" #include "game/prota.hpp" diff --git a/source/game/marcador.hpp b/source/game/marcador.hpp index d54dda0..6427c5a 100644 --- a/source/game/marcador.hpp +++ b/source/game/marcador.hpp @@ -1,6 +1,6 @@ #pragma once -#include "core/jdraw8.hpp" +#include "core/jail/jdraw8.hpp" #include "game/info.hpp" #include "game/prota.hpp" diff --git a/source/game/modulegame.cpp b/source/game/modulegame.cpp index f9014c6..d4676ec 100644 --- a/source/game/modulegame.cpp +++ b/source/game/modulegame.cpp @@ -1,10 +1,10 @@ #include "game/modulegame.hpp" -#include "core/jail_audio.hpp" -#include "core/jdraw8.hpp" -#include "core/jfile.hpp" -#include "core/jgame.hpp" -#include "core/jinput.hpp" +#include "core/jail/jail_audio.hpp" +#include "core/jail/jdraw8.hpp" +#include "core/jail/jfile.hpp" +#include "core/jail/jgame.hpp" +#include "core/jail/jinput.hpp" ModuleGame::ModuleGame() { this->gfx = JD8_LoadSurface(info::pepe_activat ? "frames2.gif" : "frames.gif"); diff --git a/source/game/modulesequence.cpp b/source/game/modulesequence.cpp index a74430c..5aaa288 100644 --- a/source/game/modulesequence.cpp +++ b/source/game/modulesequence.cpp @@ -4,11 +4,11 @@ #include -#include "core/jail_audio.hpp" -#include "core/jdraw8.hpp" -#include "core/jfile.hpp" -#include "core/jgame.hpp" -#include "core/jinput.hpp" +#include "core/jail/jail_audio.hpp" +#include "core/jail/jdraw8.hpp" +#include "core/jail/jfile.hpp" +#include "core/jail/jgame.hpp" +#include "core/jail/jinput.hpp" ModuleSequence::ModuleSequence() { } diff --git a/source/game/momia.cpp b/source/game/momia.cpp index 91d799d..74b8ddb 100644 --- a/source/game/momia.cpp +++ b/source/game/momia.cpp @@ -2,7 +2,7 @@ #include -#include "core/jgame.hpp" +#include "core/jail/jgame.hpp" Momia::Momia(JD8_Surface gfx, bool dimoni, Uint16 x, Uint16 y, Prota* sam) : Sprite(gfx) { diff --git a/source/game/options.hpp b/source/game/options.hpp index c952103..cc327b2 100644 --- a/source/game/options.hpp +++ b/source/game/options.hpp @@ -7,6 +7,22 @@ namespace Options { + // Tecles GUI (finestra, zoom) + struct KeysGUI { + SDL_Scancode dec_zoom{Defaults::KeysGUI::DEC_ZOOM}; + SDL_Scancode inc_zoom{Defaults::KeysGUI::INC_ZOOM}; + SDL_Scancode fullscreen{Defaults::KeysGUI::FULLSCREEN}; + }; + + // Tecles de joc (moviment, accions) + struct KeysGame { + SDL_Scancode up{Defaults::KeysGame::UP}; + SDL_Scancode down{Defaults::KeysGame::DOWN}; + SDL_Scancode left{Defaults::KeysGame::LEFT}; + SDL_Scancode right{Defaults::KeysGame::RIGHT}; + SDL_Scancode exit{Defaults::KeysGame::EXIT}; + }; + // Opcions d'àudio struct Audio { bool music_enabled{Defaults::Audio::MUSIC_ENABLED}; @@ -31,6 +47,8 @@ namespace Options { // --- Variables globals --- inline std::string version{}; + inline KeysGUI keys_gui{}; + inline KeysGame keys_game{}; inline Audio audio{}; inline Window window{}; inline Game game{}; diff --git a/source/game/prota.cpp b/source/game/prota.cpp index 62f99ce..f1c0168 100644 --- a/source/game/prota.cpp +++ b/source/game/prota.cpp @@ -2,8 +2,8 @@ #include -#include "core/jgame.hpp" -#include "core/jinput.hpp" +#include "core/jail/jgame.hpp" +#include "core/jail/jinput.hpp" Prota::Prota(JD8_Surface gfx) : Sprite(gfx) { diff --git a/source/game/sprite.hpp b/source/game/sprite.hpp index 1ea203c..8b3b3c8 100644 --- a/source/game/sprite.hpp +++ b/source/game/sprite.hpp @@ -1,6 +1,6 @@ #pragma once -#include "core/jdraw8.hpp" +#include "core/jail/jdraw8.hpp" struct Frame { Uint16 x; diff --git a/source/main.cpp b/source/main.cpp index e3d62dd..f227a8a 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -1,12 +1,13 @@ #include #include -#include "core/global_inputs.hpp" -#include "core/jail_audio.hpp" -#include "core/jdraw8.hpp" -#include "core/jfile.hpp" -#include "core/jgame.hpp" -#include "core/screen.hpp" +#include "core/input/global_inputs.hpp" +#include "core/jail/jail_audio.hpp" +#include "core/jail/jdraw8.hpp" +#include "core/jail/jfile.hpp" +#include "core/jail/jgame.hpp" +#include "core/rendering/overlay.hpp" +#include "core/rendering/screen.hpp" #include "game/defines.hpp" #include "game/info.hpp" #include "game/modulegame.hpp" @@ -25,6 +26,7 @@ int main(int argc, char* args[]) { Screen::init(); JD8_Init(); JA_Init(48000, SDL_AUDIO_S16, 2); + Overlay::init(); info::num_habitacio = Options::game.habitacio_inicial; info::num_piramide = Options::game.piramide_inicial; @@ -62,6 +64,7 @@ int main(int argc, char* args[]) { Options::saveToFile(); + Overlay::destroy(); JA_Quit(); JD8_Quit(); Screen::destroy();