- unitat mouse per amagar el punter

- overlay captura el esc i confirma la eixida (falla en game)
This commit is contained in:
2026-04-04 22:32:53 +02:00
parent d4fc7c0ee8
commit a4ee304a79
7 changed files with 175 additions and 44 deletions

124
CLAUDE.md
View File

@@ -18,15 +18,17 @@ cmake -B build -G "MinGW Makefiles"
cmake --build build cmake --build build
``` ```
Dependencies: SDL3. Uses CMake (minimum 3.10) with C++20. Dependencies: SDL3. Uses CMake (minimum 3.10) with C++20. SPIR-V shaders compiled automatically if `glslc` is available; precompiled headers used as fallback.
The executable is output to the project root. The `data/` folder 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 ## Architecture
### Boundary: Original vs New Code ### Golden Rule: Do Not Touch Gameplay
The codebase has a clear separation between the original game and the new modernization layer: The original game logic (gameplay, entities, map, scoring, collisions, animations) must remain untouched. All modernization work targets the presentation layer and infrastructure only. Any new feature must be implemented as an overlay on top of the existing game, never by modifying original gameplay code.
### Boundary: Original vs New Code
| Path | Owner | Rule | | Path | Owner | Rule |
|------|-------|------| |------|-------|------|
@@ -34,11 +36,10 @@ The codebase has a clear separation between the original game and the new modern
| `source/game/*.cpp/hpp` (except options/defines/defaults) | Original game | **Do not modify** | | `source/game/*.cpp/hpp` (except options/defines/defaults) | Original game | **Do not modify** |
| `source/core/rendering/` | New presentation layer | Free to modify | | `source/core/rendering/` | New presentation layer | Free to modify |
| `source/core/input/` | New input 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/utils/` | New utilities | Free to modify |
| `source/game/defines.hpp` | New constants | Free to modify | | `source/game/options,defines,defaults` | New config system | Free to modify |
| `source/game/defaults.hpp` | New defaults | Free to modify |
| `data/*.gif, *.ogg` | Original assets | **Do not modify** | | `data/*.gif, *.ogg` | Original assets | **Do not modify** |
| `data/fonts/, data/ui/` | New assets | Free to modify | | `data/fonts/, data/ui/, data/shaders/` | New assets | Free to modify |
### Original "Jail" Engine (`source/core/jail/`) ### Original "Jail" Engine (`source/core/jail/`)
@@ -47,58 +48,103 @@ Flat C-style APIs (no classes), prefixed by subsystem. **Do not touch gameplay l
- **JG** (`jgame`) — Game loop timing: init/finalize, fixed-timestep update via `JG_ShouldUpdate()` - **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()` - **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) - **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 - **JI** (`jinput`) — Input: keyboard state polling, key debouncing, cheat code detection. Filters GUI keys from game, calls `GlobalInputs::handle()` and `Mouse::updateCursorVisibility()` each update
- **JF** (`jfile`) — File I/O: filesystem folder mode (`data/`) or packed resource file (`.jrf`) - **JF** (`jfile`) — File I/O: filesystem folder mode (`data/`) or packed resource file (`.jrf`). Config folder at `~/.config/jailgames/aee/`
### Presentation Layer (`source/core/rendering/`) ### 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 - **Screen** (`screen.hpp/cpp`) — Singleton. Manages SDL_Window, SDL_Renderer, SDL_Texture. Dual rendering path: SDL3GPU with shaders (primary) or SDL_Renderer fallback. Handles fullscreen, zoom, aspect ratio 4:3, integer scaling, VSync. Counts FPS and updates render info text
- **Overlay** (`overlay.hpp/cpp`) — Paints directly on the ARGB pixel buffer before presentation. Handles notifications (slide-in animation), render info display (top/bottom/off, configurable colors), and double-ESC-to-quit logic
- **Text** (`text.hpp/cpp`) — Bitmap font renderer. Loads `.fnt` + `.gif` pairs, renders UTF-8 glyphs directly on `Uint32*` ARGB buffer. No dependency on SDL_Texture or palettes
- **SDL3GPUShader** (`sdl3gpu/`) — GPU shader backend (Vulkan/Metal). PostFX and CRT-Pi shaders with presets, supersampling (3×/6×/9×), Lanczos downscaling. Supports 4:3 aspect ratio stretch fused into the upscale pass to avoid artifacts
### Input Layer (`source/core/input/`) ### 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` - **GlobalInputs** (`global_inputs.hpp/cpp`) — Maps configurable function keys to presentation actions. Uses debounce. Returns whether a key was consumed (to suppress from game layer)
- **Mouse** (`mouse.hpp/cpp`) — Auto-hides cursor after 3 seconds of inactivity
### Configuration System (`source/game/`) ### Configuration System (`source/game/`)
Follows the pattern from `jaildoctors_dilemma`: Follows the pattern from `jaildoctors_dilemma`, persists to YAML:
- **defines.hpp** — Game-wide constants (window title, version, screen dimensions) - **defines.hpp** — Game constants: `Texts::WINDOW_TITLE`, `Texts::VERSION`, `GameScreen::WIDTH/HEIGHT`
- **defaults.hpp** — Default values for all persistent options, including key bindings (`Defaults::KeysGUI`, `Defaults::KeysGame`) - **defaults.hpp** — Default values: `Defaults::KeysGUI`, `Defaults::KeysGame`, `Defaults::Video`, `Defaults::Audio`, `Defaults::Window`, `Defaults::Game`
- **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) - **options.hpp/cpp** — `Options` namespace with inline globals and YAML load/save. Structs: `KeysGUI`, `KeysGame`, `Video`, `RenderInfo`, `Audio`, `Window`, `Game`, `PostFXPreset`, `CrtPiPreset`
### Game Modules (`source/game/`) — Original, Do Not Touch ### Utilities (`source/utils/`)
- **ModuleSequence** — Non-gameplay screens: intro, menu, slides, banners, credits, death screen (state=1) - **utils.hpp/cpp** — `toLower()` and other helpers
- **ModuleGame** — Core gameplay loop, orchestrates all game objects (state=0)
- **Sprite** — Base class for animated entities (frame/animation data via `Entitat`) ### Function Key Map
- **Prota** — Player character ("Sam"), extends Sprite
- **Mapa** — Level map with tomb grid (16 tombs), items, door logic | Key | Action |
- **Momia** — Enemy: mummies |-----|--------|
- **Bola** — Enemy: projectile ball | F1 | Decrease window zoom |
- **Marcador** — HUD/scoreboard | F2 | Increase window zoom |
- **info** — Global game state namespace (room number, pyramid, money, diamonds, lives, etc.) | F3 | Toggle fullscreen |
| F4 | Toggle shaders on/off |
| F5 | Toggle aspect ratio (square pixels ↔ 4:3 CRT) |
| F6 | Toggle supersampling |
| F7 | Cycle shader type (PostFX ↔ CRT-Pi) |
| F8 | Cycle shader presets |
| F9 | Toggle stretch filter (nearest ↔ linear) |
| F10 | Cycle render info (off → top → bottom → off) |
| ESC | Double-press to quit (with overlay notification) |
All key bindings are configurable via `Options::keys_gui` and stored in `config.yaml`.
### Rendering Pipeline
```
JD8_Flip():
1. palette→ARGB in pixel_data[320×200] (original engine)
2. Screen::present(pixel_data):
a. FPS count + render info text update
b. Overlay::render(pixel_data) (notifications, render info, directly on ARGB)
c. IF GPU + shaders enabled:
- uploadPixels → scene_texture (320×200)
- [IF 4:3] stretch pass fused with upscale: scene → scaled_texture (W×factor, H×factor×1.2)
- [IF SS] upscale pass: scene → scaled_texture (W×factor, H×factor)
- PostFX or CRT-Pi shader → swapchain (with viewport letterboxing)
d. ELSE IF GPU without shaders:
- uploadPixels → clean render → swapchain
e. ELSE (fallback):
- SDL_UpdateTexture → SDL_RenderPresent
```
### Pixel Format
JD8_Flip produces ABGR byte order: `0xFF000000 + R + (G<<8) + (B<<16)`. SDL texture uses `SDL_PIXELFORMAT_ABGR8888`. GPU textures use `SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM` (same byte layout on little-endian). Overlay colors are ABGR format.
### Persistence Files
| File | Content |
|------|---------|
| `~/.config/jailgames/aee/config.yaml` | Main config (video, audio, window, render_info, game, shader selection) |
| `~/.config/jailgames/aee/postfx.yaml` | PostFX shader presets (6 defaults: CRT, NTSC, CURVED, SCANLINES, SUBTLE, CRT LIVE) |
| `~/.config/jailgames/aee/crtpi.yaml` | CRT-Pi shader presets (4 defaults: DEFAULT, CURVED, SHARP, MINIMAL) |
### External Libraries (`source/external/`) ### External Libraries (`source/external/`)
- `gif.h` — Header-only GIF decoder - `gif.h` — Header-only GIF decoder. **Cannot be included from more than one .cpp** (no include guards on functions). Other files use `extern` declarations for `LoadGif()`
- `stb_vorbis.h` — stb single-header OGG decoder - `stb_vorbis.h` — stb single-header OGG decoder
- `fkyaml_node.hpp` — Header-only YAML parser (fkYAML v0.4.2) - `fkyaml_node.hpp` — Header-only YAML parser (fkYAML v0.4.2)
### Data Assets (`data/`) ### Data Assets (`data/`)
- `*.gif`, `*.ogg`, `crtpi.glsl` — Original game assets (**do not modify**) - `*.gif`, `*.ogg` — Original game assets (**do not modify**)
- `fonts/` — New font assets for overlay/UI - `fonts/8bithud.fnt + .gif` — Bitmap font for overlay (8×8, 124 glyphs, UTF-8 with accents)
- `ui/` — New UI graphics for overlay - `shaders/` — GLSL sources: `postfx.vert`, `postfx.frag`, `upscale.frag`, `downscale.frag`, `crtpi_frag.glsl`
- `ui/` — Reserved for future UI graphics
### Known Issues & Technical Debt
1. **ESC double-press does not work in ModuleGame**: `modulegame.cpp:136` polls `JI_KeyPressed(ESCAPE)` each frame and calls `JG_QuitSignal()` immediately. The overlay intercepts the KEY_UP event and blocks polling via `esc_blocked_`, but there is a race condition — the game's polling can fire before the block takes effect. Needs deeper integration (possibly intercepting at `JI_KeyPressed` level with state tracking across frames, or modifying the game module to use the overlay's quit flow)
2. **Overlay freezes during intro sequences**: ModuleSequence does blocking loops with delays that don't call `JI_Update()`, so `Overlay::render()` doesn't execute and notifications appear frozen. Would require refactoring the original modules to use non-blocking animation — conflicts with golden rule
3. **gif.h cannot be included twice**: Functions are not `static` or `inline`, causing multiple definition errors. Text class uses `extern` forward declarations as workaround
### Main Loop (`source/main.cpp`) ### 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. Init order: `file_setconfigfolder``Options::load``Options::loadPostFX/CrtPi``JG_Init``Screen::init``JD8_Init``JA_Init``Overlay::init`. Shutdown reverse. A state machine alternates between `ModuleSequence` (state 1) and `ModuleGame` (state 0). Each module's `Go()` returns the next state (-1 to quit).
### 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/jail/jgame.hpp"`, `#include "game/info.hpp"`)
- Headers use `.hpp` extension; external third-party headers in `source/external/` keep `.h`

View File

@@ -27,8 +27,9 @@ set(APP_SOURCES
# Core - SDL3 GPU shader backend # Core - SDL3 GPU shader backend
source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp
# Core - Input global (nueva) # Core - Input (nova capa)
source/core/input/global_inputs.cpp source/core/input/global_inputs.cpp
source/core/input/mouse.cpp
# Game # Game
source/game/options.cpp source/game/options.cpp

View File

@@ -0,0 +1,26 @@
#include "core/input/mouse.hpp"
namespace Mouse {
static constexpr Uint32 HIDE_DELAY = 3000; // Temps en ms per a amagar el cursor
static Uint32 last_move_time = 0;
static bool cursor_visible = true;
void handleEvent(const SDL_Event& event) {
if (event.type == SDL_EVENT_MOUSE_MOTION) {
last_move_time = SDL_GetTicks();
if (!cursor_visible) {
SDL_ShowCursor();
cursor_visible = true;
}
}
}
void updateCursorVisibility() {
if (cursor_visible && (SDL_GetTicks() - last_move_time > HIDE_DELAY)) {
SDL_HideCursor();
cursor_visible = false;
}
}
} // namespace Mouse

View File

@@ -0,0 +1,11 @@
#pragma once
#include <SDL3/SDL.h>
namespace Mouse {
// Gestiona el moviment del ratolí (mostra el cursor si es mou)
void handleEvent(const SDL_Event& event);
// Amaga el cursor si no s'ha mogut en un temps
void updateCursorVisibility();
} // namespace Mouse

View File

@@ -3,7 +3,9 @@
#include <string> #include <string>
#include "core/input/global_inputs.hpp" #include "core/input/global_inputs.hpp"
#include "core/input/mouse.hpp"
#include "core/jail/jgame.hpp" #include "core/jail/jgame.hpp"
#include "core/rendering/overlay.hpp"
#include "game/options.hpp" #include "game/options.hpp"
const bool* keystates; // = SDL_GetKeyboardState( NULL ); const bool* keystates; // = SDL_GetKeyboardState( NULL );
@@ -11,6 +13,7 @@ SDL_Event event;
Uint8 cheat[5]; Uint8 cheat[5];
bool key_pressed = false; bool key_pressed = false;
int waitTime = 0; int waitTime = 0;
static bool esc_blocked_ = false; // Bloqueja ESC per polling quan l'overlay l'ha consumit
// Comprova si un scancode pertany a les tecles reservades per a la GUI // Comprova si un scancode pertany a les tecles reservades per a la GUI
static bool isGuiKey(SDL_Scancode sc) { static bool isGuiKey(SDL_Scancode sc) {
@@ -47,20 +50,40 @@ void JI_Update() {
while (SDL_PollEvent(&event)) { while (SDL_PollEvent(&event)) {
if (event.type == SDL_EVENT_QUIT) JG_QuitSignal(); if (event.type == SDL_EVENT_QUIT) JG_QuitSignal();
if (event.type == SDL_EVENT_KEY_UP) { if (event.type == SDL_EVENT_KEY_UP) {
// Si és una tecla GUI, no la passem al joc legacy // ESC interceptat per l'overlay (doble pulsació per eixir)
if (!isGuiKey(event.key.scancode)) { if (event.key.scancode == SDL_SCANCODE_ESCAPE) {
if (Overlay::handleEscape()) {
// Consumit: primera pulsació, bloqueja ESC per polling
esc_blocked_ = true;
} else {
// Segona pulsació: desbloqueja i passa al joc
esc_blocked_ = false;
key_pressed = true;
JG_QuitSignal();
}
} else if (!isGuiKey(event.key.scancode)) {
// Tecles normals del joc
key_pressed = true; key_pressed = true;
JI_moveCheats(event.key.scancode); JI_moveCheats(event.key.scancode);
} }
} }
Mouse::handleEvent(event);
} }
// GlobalInputs processa les tecles GUI per polling // Desbloqueja ESC quan la tecla ja no està polsada i l'overlay ha fet timeout
if (esc_blocked_ && !keystates[SDL_SCANCODE_ESCAPE]) {
esc_blocked_ = false;
}
Mouse::updateCursorVisibility();
GlobalInputs::handle(); GlobalInputs::handle();
} }
bool JI_KeyPressed(int key) { bool JI_KeyPressed(int key) {
return waitTime > 0 ? false : (keystates[key] != 0); if (waitTime > 0) return false;
// Si ESC està bloquejat per l'overlay, no la passem al joc
if (key == SDL_SCANCODE_ESCAPE && esc_blocked_) return false;
return keystates[key] != 0;
} }
bool JI_CheatActivated(const char* cheat_code) { bool JI_CheatActivated(const char* cheat_code) {

View File

@@ -50,6 +50,9 @@ namespace Overlay {
// --- Render info --- // --- Render info ---
static std::string render_info_text_; static std::string render_info_text_;
// --- Doble ESC per a eixir ---
static bool esc_waiting_ = false;
void init() { void init() {
font_ = std::make_unique<Text>("fonts/8bithud.fnt", "fonts/8bithud.gif"); font_ = std::make_unique<Text>("fonts/8bithud.fnt", "fonts/8bithud.gif");
last_ticks_ = SDL_GetTicks(); last_ticks_ = SDL_GetTicks();
@@ -141,6 +144,11 @@ namespace Overlay {
notifications_.erase( notifications_.erase(
std::remove_if(notifications_.begin(), notifications_.end(), [](const Notification& n) { return n.status == Status::FINISHED; }), std::remove_if(notifications_.begin(), notifications_.end(), [](const Notification& n) { return n.status == Status::FINISHED; }),
notifications_.end()); notifications_.end());
// Si la notificació d'ESC ha desaparegut, reseteja l'estat
if (esc_waiting_ && notifications_.empty()) {
esc_waiting_ = false;
}
} }
void showNotification(const char* text, float duration_seconds) { void showNotification(const char* text, float duration_seconds) {
@@ -175,4 +183,16 @@ namespace Overlay {
render_info_text_ = text; render_info_text_ = text;
} }
auto handleEscape() -> bool {
if (!esc_waiting_) {
// Primera pulsació: mostra avís i consumeix
esc_waiting_ = true;
showNotification("TORNA A PULSAR ESC PER EIXIR", 2.0F);
return true; // Consumit
}
// Segona pulsació: deixa passar
esc_waiting_ = false;
return false;
}
} // namespace Overlay } // namespace Overlay

View File

@@ -15,4 +15,8 @@ namespace Overlay {
// Activa/desactiva la info de renderitzat (FPS, driver, shader, preset) // Activa/desactiva la info de renderitzat (FPS, driver, shader, preset)
void toggleRenderInfo(); void toggleRenderInfo();
void setRenderInfoText(const char* text); void setRenderInfoText(const char* text);
// Gestió d'eixida amb doble ESC
// Retorna true si l'ESC ha sigut consumit (no s'ha de passar al joc)
auto handleEscape() -> bool;
} // namespace Overlay } // namespace Overlay