Compare commits
211 Commits
v2-title-3d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 817c8fc8a0 | |||
| 3fe8fa9b32 | |||
| 65f710bf7a | |||
| 72302554ae | |||
| 03530d0439 | |||
| 705d32e919 | |||
| e420db2896 | |||
| 785700f819 | |||
| 07863577bc | |||
| 8a341be027 | |||
| 93fb914e54 | |||
| 8d659c44e5 | |||
| 5407f66c9e | |||
| dd91b07a14 | |||
| fc8233ef57 | |||
| ef2c13b011 | |||
| 69e337393a | |||
| 56c3f978d3 | |||
| cb958f33ba | |||
| e3d12e6e27 | |||
| 47e9d85708 | |||
| 82027e4975 | |||
| ab06cb32c9 | |||
| 9e7061d8b7 | |||
| b4b95c883f | |||
| a46b93c917 | |||
| 8d18c50aaa | |||
| b412435862 | |||
| 5b90a9a767 | |||
| 5ba562178b | |||
| 55b37ba594 | |||
| 20825c8138 | |||
| 9235e684e8 | |||
| 0350063fb7 | |||
| 56065995fd | |||
| 17e9206d26 | |||
| 462e91d967 | |||
| 3bc87ad652 | |||
| a7233e13df | |||
| 0abd661905 | |||
| a808226481 | |||
| 317e2a3fd9 | |||
| e4f8f586d6 | |||
| 6f29731679 | |||
| d7a9bd4ab2 | |||
| ab5489a080 | |||
| f4567a2e82 | |||
| 4b298ffc1c | |||
| 0f986cbf80 | |||
| 582bd0ee30 | |||
| 2e4030c2f2 | |||
| a9b662840b | |||
| 30bbb37bff | |||
| 2f6d6c405f | |||
| 068f42782b | |||
| 472c543c7b | |||
| 4e67a67ace | |||
| 1e63d3ae9d | |||
| b363efd1f0 | |||
| 0abbaa09f8 | |||
| 455b7a6893 | |||
| 92f76d091d | |||
| c1956e0028 | |||
| 491992a4d7 | |||
| e5b727216c | |||
| f03e337b9a | |||
| 99e99e7e08 | |||
| b93761eb1e | |||
| 4f5421191d | |||
| 71ed9dc24f | |||
| 1a0cc504c4 | |||
| 86775d4642 | |||
| b936f410ce | |||
| ddcd2076a1 | |||
| 9345facaed | |||
| 885caa6bc3 | |||
| a77bbe4420 | |||
| 61a4886e62 | |||
| 164f58c883 | |||
| fbfacb825b | |||
| 5e4d2cf993 | |||
| 97d3749269 | |||
| 0dcecf9a3c | |||
| c75e6406cd | |||
| 0254b44369 | |||
| ff11567471 | |||
| 06e383fe2c | |||
| dc5b31087a | |||
| 9e745dc3fc | |||
| 14b10c663e | |||
| f64c72f9a6 | |||
| 610eaf257e | |||
| b511740d93 | |||
| b0643b6f62 | |||
| 7e8d79222c | |||
| 14295ce859 | |||
| 5ad433e63a | |||
| 61e40e88f4 | |||
| 410955de3c | |||
| 9c0502eefb | |||
| 9b3da3a6e7 | |||
| bc41169176 | |||
| b3a1afce06 | |||
| 4b6dc8a47a | |||
| 3dadd5fc1a | |||
| bea844d51e | |||
| 5fb6c68df4 | |||
| 866a057704 | |||
| da8eab330d | |||
| 39bda0775e | |||
| ed4d3a3915 | |||
| 6447932212 | |||
| 9f278772bb | |||
| 2d073b6055 | |||
| 99b18d208d | |||
| 1321566910 | |||
| cefafe99e4 | |||
| daa7eaf811 | |||
| 3dcf5c3a99 | |||
| 99d0f62ab5 | |||
| 85050c8da4 | |||
| 120c5502fd | |||
| 64a6599e81 | |||
| a4b567588f | |||
| 2e74fea2d5 | |||
| c4933875dd | |||
| 10a54aef91 | |||
| 34be79192c | |||
| fcf13591be | |||
| 3e8f2f35bf | |||
| e5a91825b1 | |||
| b3271b17a2 | |||
| d4117e3505 | |||
| 73c7e4ea76 | |||
| 23cc5ce68d | |||
| e42059e486 | |||
| 00f40d194b | |||
| 31f348328e | |||
| 8c48a9a772 | |||
| bacfbe6eac | |||
| 63d08aef46 | |||
| 87f818ef96 | |||
| 7eafe21623 | |||
| 22827c28fa | |||
| 8c21345f14 | |||
| 56d7d4af52 | |||
| 71c43ec6fe | |||
| 443b461974 | |||
| cc16908b86 | |||
| c4c6881bd6 | |||
| 35d720bb77 | |||
| 274ce1ca63 | |||
| 252e881e93 | |||
| d36ad7d1c5 | |||
| 7305d2f5dc | |||
| 4cfad053f0 | |||
| 807f71ffa7 | |||
| d12f24d798 | |||
| f9d2539a45 | |||
| 87bfccd14f | |||
| e5e3729215 | |||
| 6210985548 | |||
| 20250a0d6d | |||
| e5616f7c3a | |||
| 3b1e469a4f | |||
| 70ca19eb87 | |||
| 7e52eaeddb | |||
| d618b6d561 | |||
| e954d4ea59 | |||
| b1ee23cd20 | |||
| d86b10c14e | |||
| 1ea38d4f6a | |||
| 26bd5a9efa | |||
| 4b0d85c010 | |||
| 149b485a9b | |||
| 6b1f064cda | |||
| 1cef6a2c23 | |||
| 007460dc51 | |||
| 10057a82de | |||
| 73fa5bf1d1 | |||
| c32b564da1 | |||
| 7b9b5ce569 | |||
| f0b3a1fbc4 | |||
| 869b4374ba | |||
| ea192cd9de | |||
| 5d30f6be68 | |||
| a342d79b86 | |||
| 1db7368c9f | |||
| 88b002b277 | |||
| 044a3a3bbf | |||
| 49070aa843 | |||
| 18e05e36e6 | |||
| bf79eecca0 | |||
| b80216dce1 | |||
| 87138f9a1f | |||
| c6560514d8 | |||
| 839f73e1ef | |||
| 2ca2062011 | |||
| 03209ee23b | |||
| c61299f17f | |||
| 880af293ef | |||
| 67c59992c9 | |||
| be3d696f60 | |||
| 6b8f6a267d | |||
| 120b8ada38 | |||
| 8bb052981d | |||
| 7fc8e48596 | |||
| ff518195f8 | |||
| 54d3e683a1 | |||
| a29c2b9cc2 | |||
| 85e7e70767 |
@@ -9,6 +9,9 @@ Checks:
|
||||
- -bugprone-easily-swappable-parameters
|
||||
- -bugprone-narrowing-conversions
|
||||
- -modernize-avoid-c-arrays
|
||||
# No forçar reemplaç de bucles "normals" per std::any_of/std::all_of.
|
||||
# Equivalent a `--suppress=useStlAlgorithm` que ja tenim a cppcheck.
|
||||
- -readability-use-anyofallof
|
||||
# performance-noexcept-move-constructor crashea clang-tidy (LLVM 19.1)
|
||||
# con recursión infinita en ExceptionSpecAnalyzer::analyzeRecord cuando
|
||||
# analiza ciertas instanciaciones de std::set. No es un falso positivo
|
||||
|
||||
+18
-9
@@ -1,5 +1,5 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(orni VERSION 0.7.2 LANGUAGES CXX)
|
||||
project(orni VERSION 0.8.1 LANGUAGES CXX)
|
||||
|
||||
# Info del projecte (font de veritat per a project.h)
|
||||
set(PROJECT_LONG_NAME "Orni Attack")
|
||||
@@ -69,7 +69,7 @@ target_link_libraries(${PROJECT_NAME} PRIVATE SDL3::SDL3)
|
||||
if(EXTERNAL_SOURCES)
|
||||
set_source_files_properties(
|
||||
${EXTERNAL_SOURCES}
|
||||
PROPERTIES COMPILE_OPTIONS "-Wno-missing-field-initializers;-Wno-deprecated-declarations"
|
||||
PROPERTIES COMPILE_OPTIONS "-Wno-missing-field-initializers;-Wno-deprecated-declarations;-Wno-tautological-compare"
|
||||
)
|
||||
endif()
|
||||
|
||||
@@ -110,7 +110,10 @@ add_executable(pack_resources EXCLUDE_FROM_ALL
|
||||
tools/pack_resources/pack_resources.cpp
|
||||
source/core/resources/resource_pack.cpp
|
||||
)
|
||||
target_include_directories(pack_resources PRIVATE "${CMAKE_SOURCE_DIR}/source")
|
||||
target_include_directories(pack_resources PRIVATE
|
||||
"${CMAKE_SOURCE_DIR}/source"
|
||||
"${CMAKE_BINARY_DIR}"
|
||||
)
|
||||
target_compile_options(pack_resources PRIVATE -Wall -Wextra -Wpedantic)
|
||||
|
||||
# --- REGENERACIÓ AUTOMÀTICA DE build/resources.pack ---
|
||||
@@ -135,9 +138,8 @@ add_dependencies(${PROJECT_NAME} resource_pack)
|
||||
|
||||
# --- COMPILACIÓ DE SHADERS GLSL → SPIR-V (headers C++ embedits) ---
|
||||
# Compila els shaders .glsl a SPIR-V i els converteix en headers C++ embedits
|
||||
# (source/core/rendering/gpu/spv/*.h). Aquests headers es commiteen al repo:
|
||||
# en macOS no cal glslc (els headers ja existeixen). En Linux/Windows glslc
|
||||
# és obligatori per regenerar els headers en cada canvi del GLSL.
|
||||
# (source/core/rendering/gpu/spv/*.h). Aquests headers es commiteen al repo,
|
||||
# així que glslc només és necessari quan canvien els .glsl o falten headers.
|
||||
#
|
||||
# Per a macOS hi ha a més els headers MSL escrits a mà a source/core/rendering/gpu/msl/.
|
||||
set(SHADERS_DIR "${CMAKE_SOURCE_DIR}/shaders")
|
||||
@@ -156,6 +158,13 @@ set(ALL_SHADER_SOURCES
|
||||
"${SHADERS_DIR}/postfx.frag.glsl"
|
||||
"${SHADERS_DIR}/bloom.frag.glsl"
|
||||
)
|
||||
set(ALL_SHADER_HEADERS_PRESENT TRUE)
|
||||
foreach(_spv_header IN LISTS ALL_SHADER_HEADERS)
|
||||
if(NOT EXISTS "${_spv_header}")
|
||||
set(ALL_SHADER_HEADERS_PRESENT FALSE)
|
||||
break()
|
||||
endif()
|
||||
endforeach()
|
||||
find_program(GLSLC_EXE NAMES glslc HINTS ${Vulkan_GLSLC_EXECUTABLE})
|
||||
if(GLSLC_EXE)
|
||||
add_custom_command(
|
||||
@@ -172,10 +181,10 @@ if(GLSLC_EXE)
|
||||
add_custom_target(shaders DEPENDS ${ALL_SHADER_HEADERS})
|
||||
add_dependencies(${PROJECT_NAME} shaders)
|
||||
message(STATUS "Shaders: glslc trobat (${GLSLC_EXE}); headers SPV es regeneraran si canvia el GLSL")
|
||||
elseif(APPLE)
|
||||
message(STATUS "Shaders: glslc no trobat en macOS — s'usaran els headers SPV ja commiteats")
|
||||
elseif(ALL_SHADER_HEADERS_PRESENT)
|
||||
message(STATUS "Shaders: glslc no trobat — s'usaran els headers SPV ja commiteats al repo")
|
||||
else()
|
||||
message(FATAL_ERROR "glslc no trobat: instal·la 'shaderc' o 'vulkan-sdk' per compilar shaders SPIR-V (obligatori a Linux/Windows)")
|
||||
message(FATAL_ERROR "glslc no trobat i falten headers SPV: instal·la 'shaderc' o 'vulkan-sdk' per generar-los")
|
||||
endif()
|
||||
|
||||
# --- STATIC ANALYSIS / FORMAT TARGETS ---
|
||||
|
||||
@@ -0,0 +1,838 @@
|
||||
# Arquitectura de Orni Attack
|
||||
|
||||
> Documento de orientación para alguien que llega nuevo al proyecto. Cada
|
||||
> afirmación está anclada a código real (fichero/clase/función con su ruta).
|
||||
> Cuando algo no se ha podido verificar o no existe, se indica explícitamente.
|
||||
> El objetivo no es vender una arquitectura ideal, sino describir lo que **este**
|
||||
> proyecto hace, incluso donde es poco convencional.
|
||||
|
||||
## Índice
|
||||
|
||||
1. [Visión general](#1-visión-general)
|
||||
2. [Punto de entrada y el Director](#2-punto-de-entrada-y-el-director)
|
||||
3. [Bucle principal](#3-bucle-principal)
|
||||
4. [Sistema de escenas](#4-sistema-de-escenas)
|
||||
5. [Renderizado: de la lógica al píxel](#5-renderizado-de-la-lógica-al-píxel)
|
||||
6. [Entrada](#6-entrada)
|
||||
7. [Audio](#7-audio)
|
||||
8. [Recursos](#8-recursos)
|
||||
9. [Comunicación entre módulos](#9-comunicación-entre-módulos)
|
||||
10. [Lógica del juego](#10-lógica-del-juego)
|
||||
11. [IA del modo demo (attract)](#11-ia-del-modo-demo-attract)
|
||||
12. [Efectos visuales](#12-efectos-visuales)
|
||||
13. [Configuración, constantes y convenciones](#13-configuración-constantes-y-convenciones)
|
||||
14. [Guía de navegación](#14-guía-de-navegación)
|
||||
|
||||
---
|
||||
|
||||
## 1. Visión general
|
||||
|
||||
Orni Attack es un arcade vectorial (estética CRT de líneas con bloom) construido
|
||||
sobre **SDL3**, usando la **GPU API de SDL3** (`SDL_gpu`) para el render — **no**
|
||||
`SDL_Renderer`. El código está partido en dos grandes mundos:
|
||||
|
||||
- **`source/core/`** — el "motor": ventana, GPU, audio, input, recursos, i18n,
|
||||
overlays de sistema. No conoce nada del juego concreto. Por ejemplo,
|
||||
[audio.hpp](source/core/audio/audio.hpp) recibe un struct de configuración y no
|
||||
lee YAML, e [input.hpp](source/core/input/input.hpp) no incluye nada de `game/`.
|
||||
- **`source/game/`** — la lógica concreta de Orni Attack: escenas, entidades
|
||||
(naves, enemigos, balas), sistemas (colisiones, IA), stages/oleadas y efectos.
|
||||
|
||||
El punto de indirección entre ambos mundos para el render es
|
||||
[render_context.hpp](source/core/rendering/render_context.hpp): el juego habla con
|
||||
un `Rendering::Renderer*` opaco que es un alias de `GPU::GpuFrameRenderer`. Esto
|
||||
permite cambiar de backend sin tocar las firmas del juego.
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph entry["Punto de entrada"]
|
||||
MAIN["main.cpp<br/>SDL_MAIN_USE_CALLBACKS"]
|
||||
end
|
||||
MAIN -->|posee| DIR["Director<br/>(es el programa)"]
|
||||
|
||||
subgraph core["source/core (motor)"]
|
||||
SDLM["SDLManager<br/>ventana + GPU"]
|
||||
GE["GlobalEvents<br/>F1-F7/F12/ESC/hotplug"]
|
||||
INPUT["Input (singleton)"]
|
||||
AUDIO["Audio (singleton)"]
|
||||
RES["Resource::Loader / Pack"]
|
||||
LOC["Locale (i18n)"]
|
||||
OVL["Notifier · ServiceMenu<br/>DebugOverlay · DefineInputs"]
|
||||
end
|
||||
|
||||
subgraph game["source/game (juego)"]
|
||||
SCN["Scenes<br/>Logo · Title · Game"]
|
||||
ENT["Entities<br/>Ship · Enemy · Bullet"]
|
||||
SYS["Systems<br/>Collision · EnemyAi · DemoPilot"]
|
||||
STG["StageManager / WaveRunner"]
|
||||
FX["Effects<br/>debris · firework · score · trail"]
|
||||
end
|
||||
|
||||
DIR --> SDLM
|
||||
DIR --> GE
|
||||
DIR --> OVL
|
||||
DIR --> SCN
|
||||
SCN --> ENT
|
||||
SCN --> SYS
|
||||
SCN --> STG
|
||||
SCN --> FX
|
||||
GE --> INPUT
|
||||
SCN -.usa.-> AUDIO
|
||||
SCN -.usa.-> RES
|
||||
OVL -.usa.-> LOC
|
||||
```
|
||||
|
||||
**Patrón dominante de comunicación:** singletons globales (`Input::get()`,
|
||||
`Audio::get()`, `Locale::get()`, `Notifier`, `ServiceMenu`) más paso por
|
||||
referencia de un `Rendering::Renderer*` y un `SceneContext&`. **No hay** un bus de
|
||||
eventos genérico ni un ECS — las entidades viven en `std::array` de tamaño fijo
|
||||
dentro de `GameScene` y los sistemas operan sobre un struct `Context` de punteros
|
||||
(ver [§10](#10-lógica-del-juego)).
|
||||
|
||||
**Rasgo de diseño destacable:** gran parte de la lógica es *data-driven*. Los
|
||||
enemigos, balas y el jugador se describen en **YAML declarativo**
|
||||
(`data/entities/*/*.yaml`: physics/ai/animation/events), los stages en
|
||||
`data/stages/stages.yaml` (oleadas), y las figuras vectoriales en ficheros `.shp`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Punto de entrada y el Director
|
||||
|
||||
El `main` real está en [main.cpp](source/main.cpp) y usa el modo de callbacks de
|
||||
SDL3 (`#define SDL_MAIN_USE_CALLBACKS 1`). En lugar de un bucle `while` clásico,
|
||||
SDL llama a cuatro funciones, y todas son pura fontanería que delega en un
|
||||
`Director`:
|
||||
|
||||
```cpp
|
||||
// main.cpp
|
||||
auto SDL_AppInit(void** appstate, int argc, char* argv[]) -> SDL_AppResult {
|
||||
System::Relaunch::setArgv(argc, argv);
|
||||
auto director = std::make_unique<Director>(argc, argv);
|
||||
*appstate = director.release(); // SDL guarda el puntero
|
||||
return SDL_APP_CONTINUE;
|
||||
}
|
||||
auto SDL_AppEvent(void* s, SDL_Event* e) { return ((Director*)s)->handleEvent(*e); }
|
||||
auto SDL_AppIterate(void* s) { return ((Director*)s)->iterate(); }
|
||||
void SDL_AppQuit(void* s, ...) { /* reabsorbe y destruye el Director */ }
|
||||
```
|
||||
|
||||
La filosofía está escrita en el propio comentario de cabecera de
|
||||
[director.hpp](source/core/system/director.hpp):
|
||||
|
||||
> *El Director és EL programa: posseeix la configuració, els subsistemes i
|
||||
> l'estat.*
|
||||
|
||||
Como con `SDL_MAIN_USE_CALLBACKS` no hay un `scope` que envuelva todo el bucle,
|
||||
el estado que antes vivía en un `run()` ahora es **miembro** del Director:
|
||||
`sdl_` (SDLManager), `context_` (SceneContext), `debug_overlay_` y
|
||||
`current_scene_` (todos `std::unique_ptr`, ver
|
||||
[director.hpp:45-48](source/core/system/director.hpp#L45-L48)).
|
||||
|
||||
### Orden de arranque (constructor)
|
||||
|
||||
El constructor [Director::Director](source/core/system/director.cpp#L46) ejecuta el
|
||||
bootstrap completo, en este orden:
|
||||
|
||||
1. `ConfigYaml::init()` — valores por defecto de configuración.
|
||||
2. Parseo de argumentos (`--console`, `--reset-config`) en
|
||||
[checkProgramArguments](source/core/system/director.cpp#L241).
|
||||
3. `Utils::initializePathSystem()` + sistema de recursos
|
||||
([§8](#8-recursos)): en *release* el `resources.pack` es obligatorio; en *dev*
|
||||
hay fallback a `data/`.
|
||||
4. Crea la carpeta de sistema (`~/.config/jailgames/<NAME>` en Linux) y carga/crea
|
||||
`config.yaml` ([createSystemFolder](source/core/system/director.cpp#L260)).
|
||||
5. Carga el `locale` ([§7](#7-audio) usa lo mismo: i18n).
|
||||
6. `Input::init()` con el `gamecontrollerdb.txt` (autoasigna mandos a P1/P2 la
|
||||
primera vez).
|
||||
7. Crea `SDLManager` (ventana + GPU), oculta el cursor, inicializa `Audio`.
|
||||
8. **Precarga bloqueante** de todos los recursos (música, sonidos, shapes) para
|
||||
evitar tirones de I/O en las transiciones
|
||||
([director.cpp:187-195](source/core/system/director.cpp#L187-L195)).
|
||||
9. Crea el `SceneContext` y fija la escena inicial: `TITLE` en `_DEBUG`, `LOGO`
|
||||
en el resto ([director.cpp:200-205](source/core/system/director.cpp#L200-L205)).
|
||||
10. Inicializa los overlays de sistema: `DebugOverlay`, `Notifier`, `ServiceMenu`,
|
||||
`DefineInputs`.
|
||||
|
||||
El destructor [Director::~Director](source/core/system/director.cpp#L218) guarda
|
||||
la config y destruye los subsistemas **en orden inverso** a la construcción (el
|
||||
`Notifier` referencia el renderer, así que debe morir antes que `sdl_`).
|
||||
|
||||
---
|
||||
|
||||
## 3. Bucle principal
|
||||
|
||||
Cada frame, SDL llama a `SDL_AppIterate`, que delega en
|
||||
[Director::iterate()](source/core/system/director.cpp#L383). Su estructura es:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant SDL
|
||||
participant Dir as Director::iterate()
|
||||
participant Scene
|
||||
participant SDLM as SDLManager
|
||||
participant GPU as GpuFrameRenderer
|
||||
|
||||
SDL->>Dir: iterate()
|
||||
Note over Dir: si wants_quit_ → SDL_APP_SUCCESS
|
||||
Dir->>Dir: si !scene o scene.isFinished() → advanceScene()
|
||||
Dir->>Dir: delta_time = (now - last) capeado a 50 ms
|
||||
Dir->>Dir: Input::update()
|
||||
Dir->>Scene: update(dt)
|
||||
Dir->>Dir: overlays.update(dt) + Audio::update()
|
||||
Dir->>SDLM: clear() (= GPU.beginFrame)
|
||||
alt swapchain no disponible
|
||||
SDLM-->>Dir: false → saltar draw+present
|
||||
end
|
||||
Dir->>SDLM: updateRenderingContext()
|
||||
Dir->>Scene: draw()
|
||||
Dir->>Dir: overlays.draw() (capas)
|
||||
Dir->>SDLM: present() (= GPU.endFrame → bloom + postfx)
|
||||
```
|
||||
|
||||
Puntos concretos a tener en cuenta:
|
||||
|
||||
- **Pivot de escena**: si no hay escena o la actual reporta `isFinished()`, se
|
||||
llama a [advanceScene()](source/core/system/director.cpp#L338), que destruye la
|
||||
actual y construye la siguiente con
|
||||
[buildScene()](source/core/system/director.cpp#L323) según
|
||||
`context_->nextScene()`.
|
||||
- **Delta time**: se mide con `SDL_GetTicks()` y se **capea a 50 ms** para evitar
|
||||
saltos grandes tras un stall ([director.cpp:397-400](source/core/system/director.cpp#L397-L400)).
|
||||
- **Orden de update**: `Input::update()` → `current_scene_->update(dt)` →
|
||||
`debug_overlay_` → `Notifier` → `ServiceMenu` → `DefineInputs` → `Audio::update()`.
|
||||
- **Render por capas** (de abajo arriba, entre `clear` y `present`):
|
||||
escena → `debug_overlay_` → `Notifier` (toasts) → `ServiceMenu` → `DefineInputs`
|
||||
(modal de rebinding). Si el overlay de rebinding está activo, el menú de servicio
|
||||
no se pinta ([director.cpp:432-439](source/core/system/director.cpp#L432-L439)).
|
||||
- **Salto de frame**: si `sdl_->clear()` devuelve `false` (swapchain no disponible,
|
||||
p. ej. ventana minimizada), se omiten `draw` y `present` ese frame.
|
||||
|
||||
El bucle de eventos vive aparte, en
|
||||
[Director::handleEvent()](source/core/system/director.cpp#L354), que enruta cada
|
||||
`SDL_Event` por la cadena: **ventana → GlobalEvents → F11 (debug overlay) →
|
||||
escena** (ver [§9](#9-comunicación-entre-módulos)).
|
||||
|
||||
---
|
||||
|
||||
## 4. Sistema de escenas
|
||||
|
||||
La interfaz base es [scene.hpp](source/core/system/scene.hpp). Como dice su
|
||||
cabecera, *el frame loop vive en el Director, no en cada escena*. Cada escena
|
||||
implementa cuatro métodos puros:
|
||||
|
||||
```cpp
|
||||
virtual void handleEvent(const SDL_Event&) = 0; // eventos no-globales
|
||||
virtual void update(float delta_time) = 0; // lógica
|
||||
virtual void draw() = 0; // pintado (entre clear y present)
|
||||
virtual auto isFinished() const -> bool = 0; // ¿transición pendiente?
|
||||
```
|
||||
|
||||
Una escena pide transición vía `context_.setNextScene(...)`; en el siguiente frame
|
||||
`isFinished()` devuelve `true` y el Director la destruye para construir la
|
||||
siguiente.
|
||||
|
||||
### SceneContext
|
||||
|
||||
[scene_context.hpp](source/core/system/scene_context.hpp) es el "buzón" de
|
||||
transición que el Director posee y va pasando a cada escena por referencia. Tiene:
|
||||
|
||||
- `SceneType` (enum): `LOGO`, `TITLE`, `GAME`, `EXIT`.
|
||||
- `Option` (p. ej. `JUMP_TO_TITLE_MAIN`) consumible con `consumeOption()`.
|
||||
- `MatchConfig` (jugadores activos, modo NORMAL/DEMO) para pasar a `GAME`.
|
||||
- El **índice del escenario de demo** (`demoScenarioIndex()` / `advanceDemoScenario()`),
|
||||
que persiste entre escenas para que cada entrada al attract mode muestre el
|
||||
siguiente escenario curado (ver [§11](#11-ia-del-modo-demo-attract)).
|
||||
|
||||
Existe además una variable global `SceneManager::actual` que el Director mantiene
|
||||
sincronizada con la escena en curso (compatibilidad hacia atrás).
|
||||
|
||||
### Las tres escenas (FSM jerárquica)
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> LOGO
|
||||
LOGO --> TITLE
|
||||
TITLE --> GAME : START (1P/2P)
|
||||
TITLE --> GAME : idle timeout (DEMO)
|
||||
GAME --> TITLE : game over / fin demo (input)
|
||||
GAME --> LOGO : fin demo (timeout/muerte)
|
||||
TITLE --> [*] : EXIT
|
||||
```
|
||||
|
||||
Cada escena tiene además su **propia** máquina de estados interna:
|
||||
|
||||
- **[LogoScene](source/game/scenes/logo_scene.hpp)** — `AnimationState`:
|
||||
`PRE_ANIMATION → ANIMATION → POST_ANIMATION → EXPLOSION → POST_EXPLOSION`. Anima
|
||||
el logo JAILGAMES y lo hace explotar en fragmentos (debris).
|
||||
- **[TitleScene](source/game/scenes/title_scene.hpp)** — `TitleState`:
|
||||
`STARFIELD_FADE_IN → STARFIELD → MAIN → PLAYER_JOIN_PHASE → BLACK_SCREEN →
|
||||
DEMO_DIVE → DEMO_CURTAIN`. Naves 3D flotantes (vía
|
||||
[ShipAnimator](source/game/title/ship_animator.hpp)), selección 1P/2P, y un
|
||||
`idle_timer_` en el estado `MAIN` que dispara el attract mode por inactividad.
|
||||
- **[GameScene](source/game/scenes/game_scene.hpp)** — es el núcleo del juego y se
|
||||
detalla en [§10](#10-lógica-del-juego).
|
||||
|
||||
---
|
||||
|
||||
## 5. Renderizado: de la lógica al píxel
|
||||
|
||||
Este es el subsistema más denso. La idea central: **toda la geometría son líneas**
|
||||
(la estética es vectorial). El juego acumula líneas en CPU durante `draw()`, y al
|
||||
final del frame se envían a la GPU en un único batch, se rasterizan a una textura
|
||||
*offscreen*, y un par de pases de post-procesado (bloom + flicker/fondo) componen
|
||||
la imagen final sobre la swapchain.
|
||||
|
||||
### 5.1 Capas del subsistema
|
||||
|
||||
| Fichero | Rol |
|
||||
|---|---|
|
||||
| [sdl_manager.hpp/.cpp](source/core/rendering/sdl_manager.hpp) | Crea la ventana SDL, posee el `GpuFrameRenderer`, gestiona zoom/fullscreen/letterbox. Expone `clear()` / `present()` / `getRenderer()`. |
|
||||
| [gpu/gpu_frame_renderer.hpp/.cpp](source/core/rendering/gpu/gpu_frame_renderer.hpp) | Orquestador del frame GPU: `beginFrame` → `pushLine`/`pushRect` → `endFrame` (`flushBatch` + `bloomPass` + `compositePass`). |
|
||||
| [gpu/gpu_device](source/core/rendering/gpu/gpu_device.hpp) | Wrapper del `SDL_GPUDevice` (claim de ventana, formato de swapchain). |
|
||||
| [gpu/gpu_line_pipeline](source/core/rendering/gpu/gpu_line_pipeline.hpp) | Pipeline de líneas: dibuja cada línea como un quad (2 triángulos) con antialias geométrico. |
|
||||
| [gpu/gpu_bloom_pipeline](source/core/rendering/gpu/gpu_bloom_pipeline.hpp) | Blur gaussiano separable (pase H + pase V) sobre dos texturas ping-pong. |
|
||||
| [gpu/gpu_postfx_pipeline](source/core/rendering/gpu/gpu_postfx_pipeline.hpp) | Composite final: mezcla escena + bloom + flicker + fondo pulsante. |
|
||||
| [line_renderer.hpp/.cpp](source/core/rendering/line_renderer.hpp) | API que usa el juego: `Rendering::linea(...)` y `lineaGlow(...)`. |
|
||||
| [shape_renderer.hpp/.cpp](source/core/rendering/shape_renderer.hpp) | `renderShape(...)`: dibuja una `Shape` aplicando transformación y, opcionalmente, glow multipase. |
|
||||
|
||||
### 5.2 Una `Shape` y cómo se carga
|
||||
|
||||
Una "shape" es una figura vectorial: un conjunto de **polilíneas** y **líneas**
|
||||
([shape.hpp](source/core/graphics/shape.hpp)). Los ficheros viven en `data/shapes/`
|
||||
con extensión `.shp` y un formato de texto tipo clave:valor. Ejemplo real
|
||||
([data/shapes/ship/arrow.shp](data/shapes/ship/arrow.shp)):
|
||||
|
||||
```
|
||||
name: arrow
|
||||
scale: 1.0
|
||||
center: 0, 0
|
||||
polyline: 0,-12 8.49,8.49 0,4 -8.49,8.49 0,-12
|
||||
```
|
||||
|
||||
> Nota: el formato real usa directivas `name:`, `scale:`, `center:`,
|
||||
> `polyline:` y `line:` (Y negativo = arriba). No es la sintaxis
|
||||
> `POLYLINE: (x,y)` que podría suponerse de otros motores.
|
||||
|
||||
La carga la centraliza [shape_loader.hpp](source/core/graphics/shape_loader.hpp)
|
||||
(`Graphics::ShapeLoader::load(filename)`), con caché de `std::shared_ptr<Shape>`.
|
||||
Todas las shapes se precargan en el boot del Director.
|
||||
|
||||
### 5.3 El flujo de un frame de render
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A["Scene::draw()<br/>(acumula en CPU)"] --> B["Rendering::linea / renderShape"]
|
||||
B --> C["GpuFrameRenderer::pushLine()<br/>extruye quad → vertices_ / indices_"]
|
||||
C -.repetido N veces.-> C
|
||||
A --> D["SDLManager::present()<br/>= GpuFrameRenderer::endFrame()"]
|
||||
D --> E["flushBatch()<br/>sube VBO/IBO, dibuja sobre OFFSCREEN"]
|
||||
E --> F["bloomPass()<br/>H: high-pass+blur → bloom_a<br/>V: blur → bloom_b"]
|
||||
F --> G["compositePass()<br/>offscreen + bloom_b + flicker + fondo<br/>→ swapchain (letterbox)"]
|
||||
G --> H["SubmitGPUCommandBuffer + present"]
|
||||
```
|
||||
|
||||
Paso a paso, con anclas reales:
|
||||
|
||||
1. **Emisión (juego).** Durante `current_scene_->draw()`, el juego llama a
|
||||
[Rendering::linea()](source/core/rendering/line_renderer.hpp#L33) (y
|
||||
`renderShape`, `VectorText`, `Playfield`, etc.). Las coordenadas son **lógicas
|
||||
(1280×720)**. El color por defecto si `alpha==0` es el verde fósforo CRT
|
||||
`DEFAULT_LINE_COLOR = {100,255,100,255}`.
|
||||
2. **Acumulación (CPU).** `linea()` pre-multiplica el brillo y llama a
|
||||
[GpuFrameRenderer::pushLine()](source/core/rendering/gpu/gpu_frame_renderer.hpp#L88),
|
||||
que **extruye** la línea en un quad (4 vértices, 6 índices) y lo acumula en
|
||||
`vertices_` / `indices_`. Si el antialias está activo, añade ~0.5 px de padding y
|
||||
marca `edge_dist` para el fade del fragment shader.
|
||||
3. **Flush (GPU).** En `endFrame()`, `flushBatch()` sube el batch a un VBO/IBO,
|
||||
abre un render pass sobre el `offscreen_texture_` (R8G8B8A8, tamaño físico
|
||||
configurable, independiente del lógico) y dibuja con el `line_pipeline_`. El
|
||||
vertex shader transforma píxeles lógicos → NDC; el fragment shader aplica
|
||||
`smoothstep` sobre `edge_dist` para el suavizado.
|
||||
4. **Bloom.** `bloomPass()` hace un blur separable: pase H (high-pass por
|
||||
luminancia + blur horizontal → `bloom_texture_a_`) y pase V (blur vertical →
|
||||
`bloom_texture_b_`). Parámetros en `PostFxParams`
|
||||
([gpu_frame_renderer.hpp:33-51](source/core/rendering/gpu/gpu_frame_renderer.hpp#L33-L51)).
|
||||
5. **Composite.** `compositePass()` dibuja un triángulo *fullscreen* sobre la
|
||||
swapchain, muestreando offscreen + bloom, aplicando flicker temporal y un fondo
|
||||
verde pulsante. Aquí se aplica el **letterbox** vía el viewport físico
|
||||
(`setViewport`).
|
||||
|
||||
El interruptor maestro de post-proceso es **F6** (`setPostFxEnabled`): cuando está
|
||||
OFF, la escena offscreen sale tal cual (passthrough), útil para A/B testing.
|
||||
|
||||
### 5.4 Texto, 3D y elementos de escena
|
||||
|
||||
- **[VectorText](source/core/graphics/vector_text.hpp)** — renderiza texto donde
|
||||
cada carácter es una `Shape` precargada.
|
||||
- **[Camera3D](source/core/graphics/camera3d.hpp)** + **[Wireframe3D](source/core/graphics/wireframe3d.hpp)**
|
||||
— proyección perspectiva en CPU de mallas 3D (vértices + aristas) a líneas 2D.
|
||||
Lo usan el starfield 3D y las naves del título.
|
||||
- **[Starfield](source/core/graphics/starfield.hpp)** (campo de estrellas 3D que
|
||||
vienen hacia la cámara) y **[StarfieldParallax](source/core/graphics/starfield_parallax.hpp)**
|
||||
(capas 2D de fondo con parallax).
|
||||
- **[Playfield](source/core/graphics/playfield.hpp)** — rejilla de fondo con
|
||||
animación de construcción y *ripples* (ondas) que reaccionan a la nave y a las
|
||||
explosiones.
|
||||
- **[Border](source/core/graphics/border.hpp)** — marco de 4 lados que se desplaza
|
||||
al recibir impactos.
|
||||
- **[Curtain](source/core/graphics/curtain.hpp)** — cortinilla negra para
|
||||
transiciones; se pinta siempre la última.
|
||||
|
||||
### 5.5 Shaders: fuentes, compilación y selección
|
||||
|
||||
Las fuentes GLSL viven en [shaders/](shaders/): `line.vert.glsl`, `line.frag.glsl`,
|
||||
`postfx.vert.glsl`, `postfx.frag.glsl`, `bloom.frag.glsl`. **No se cargan de disco en
|
||||
runtime**: se embeben como arrays/strings en el binario.
|
||||
|
||||
**Pipeline de compilación (SPIR-V, Linux/Windows).** Lo orquesta
|
||||
[CMakeLists.txt:139-187](CMakeLists.txt#L139). La lógica clave:
|
||||
|
||||
- Para cada `.glsl` hay un header destino en
|
||||
[gpu/spv/](source/core/rendering/gpu/spv/) (p. ej. `line_vert_spv.h`).
|
||||
- CMake busca `glslc` (`find_program(GLSLC_EXE ...)`). Hay **tres caminos**:
|
||||
1. `glslc` presente → un `add_custom_command` regenera los headers SPV cuando
|
||||
cambian los `.glsl`, vía el target `shaders` del que depende el ejecutable.
|
||||
2. `glslc` ausente pero **los headers ya están commiteados** → se usan tal cual
|
||||
(los `.spv.h` están versionados en el repo).
|
||||
3. `glslc` ausente **y** faltan headers → `FATAL_ERROR` pidiendo instalar
|
||||
`shaderc`/`vulkan-sdk`.
|
||||
- La conversión binario→header la hace el script
|
||||
[tools/shaders/compile_spirv.cmake](tools/shaders/compile_spirv.cmake): invoca
|
||||
`glslc -O -fshader-stage=<vert|frag>` para producir el `.spv`, lee el binario como
|
||||
hex (`file(READ ... HEX)`) y escribe un header con
|
||||
`static const uint8_t LINE_VERT_SPV[] = { 0x.., ... };` y su `_SIZE`. Es
|
||||
multiplataforma puro CMake (no necesita `bash` ni `xxd`).
|
||||
|
||||
**MSL (macOS).** Los headers Metal en [gpu/msl/](source/core/rendering/gpu/msl/)
|
||||
(`line_vert.msl.h`, etc.) están **escritos a mano** (no los genera CMake), como
|
||||
strings literales C++.
|
||||
|
||||
**Selección SPV vs MSL: es _compile-time_, no runtime.** La hace
|
||||
[shader_factory.hpp](source/core/rendering/gpu/shader_factory.hpp) con `#ifdef __APPLE__`:
|
||||
en Apple expone `createShaderMSL(...)` (`SDL_GPU_SHADERFORMAT_MSL`), y en el resto
|
||||
`createShaderSPIRV(...)` (`SDL_GPU_SHADERFORMAT_SPIRV`). Cada pipeline llama al helper
|
||||
disponible con el header embebido correspondiente. (Es decir: no es `GpuDevice` quien
|
||||
elige el backend de shader, sino el preprocesador al compilar.)
|
||||
|
||||
---
|
||||
|
||||
## 6. Entrada
|
||||
|
||||
El subsistema de input ([core/input/](source/core/input/)) es un **singleton**
|
||||
(`Input::init()` / `Input::get()` / `Input::destroy()`) que unifica teclado,
|
||||
gamepads y ratón.
|
||||
|
||||
- **Acciones**: enum `InputAction` (`LEFT`, `RIGHT`, `THRUST`, `SHOOT`, `START`,
|
||||
`MENU`, ...) en [input_types.hpp](source/core/input/input_types.hpp).
|
||||
- **Bindings por jugador**: hay bindings separados de teclado y de gamepad para P1
|
||||
y P2, que se cargan de la config con `applyPlayer1Bindings()` /
|
||||
`applyPlayer2Bindings()` (llamados desde el constructor del Director).
|
||||
- **Captura por frame**: `Input::update()` lee `SDL_GetKeyboardState()` y los ejes
|
||||
y botones del gamepad, y hace *edge-detection* para distinguir `just_pressed` de
|
||||
`is_held`. La consulta es `checkAction(...)` / `checkActionPlayer1/2(...)`.
|
||||
- **Hotplug**: `Input::handleEvent()` procesa `SDL_EVENT_GAMEPAD_ADDED/REMOVED`
|
||||
(`addGamepad` / `removeGamepad`) y notifica con un toast vía `Notifier`.
|
||||
- **Ratón**: [mouse.hpp](source/core/input/mouse.hpp) auto-oculta el cursor.
|
||||
- **Rebinding en runtime**: [define_inputs.hpp](source/core/input/define_inputs.hpp)
|
||||
es un modal singleton que captura una secuencia de acciones, persiste en config y
|
||||
reaplica bindings sin reiniciar.
|
||||
|
||||
El enrutado de input ocurre en dos sitios: los eventos **globales** pasan por
|
||||
`GlobalEvents::handle()` (que primero deja a `Input` procesar el hotplug), y la
|
||||
lógica de juego consulta directamente `Input::get()->checkAction...` durante
|
||||
`update()` (p. ej. [Ship::processInput](source/game/entities/ship.hpp)).
|
||||
|
||||
---
|
||||
|
||||
## 7. Audio
|
||||
|
||||
[core/audio/](source/core/audio/) es otro singleton (`Audio::init/get/destroy`)
|
||||
con un motor de bajo nivel propio:
|
||||
|
||||
- **[Audio](source/core/audio/audio.hpp)** — capa lógica: `playMusic()`,
|
||||
`playSound()`, volúmenes por grupo (`GAME`, `INTERFACE`), `playSoundWithEcho/Reverb`.
|
||||
- **[jail_audio.hpp](source/core/audio/jail_audio.hpp)** (`Ja::Engine`) — motor
|
||||
sobre SDL3 audio: streaming de **OGG** (vía `stb_vorbis`) para música, **WAV**
|
||||
descomprimido para efectos, mezcla en N canales.
|
||||
- **[audio_adapter.hpp](source/core/audio/audio_adapter.hpp)** —
|
||||
`AudioResource::getMusic/getSound`: caché *lazy* que carga bytes vía
|
||||
`Resource::Helper` y los decodifica una sola vez.
|
||||
- **[audio_effects.hpp](source/core/audio/audio_effects.hpp)** — DSP de echo y
|
||||
reverb; presets en `data/config/sounds.yaml`
|
||||
([sound_effects_config.hpp](source/core/audio/sound_effects_config.hpp)).
|
||||
|
||||
El Director precarga toda la música y todos los sonidos en el boot, y llama a
|
||||
`Audio::update()` una vez por frame.
|
||||
|
||||
---
|
||||
|
||||
## 8. Recursos
|
||||
|
||||
[core/resources/](source/core/resources/) abstrae de dónde salen los bytes:
|
||||
|
||||
- **[resource_pack](source/core/resources/resource_pack.hpp)** (`Resource::Pack`)
|
||||
— lee un fichero empaquetado con cabecera *magic* `"ORNI"` y entradas con CRC32
|
||||
para validación de integridad.
|
||||
- **[resource_loader](source/core/resources/resource_loader.hpp)**
|
||||
(`Resource::Loader`, singleton Meyers) — `loadResource()`, `resourceExists()`,
|
||||
`listResources(prefix)`, `validatePack()`.
|
||||
- **[resource_helper](source/core/resources/resource_helper.hpp)** — wrappers de
|
||||
conveniencia (`initializeResourceSystem`, `listResources`, `loadFile`).
|
||||
|
||||
**Estrategia dual** (decidida en el constructor del Director,
|
||||
[director.cpp:64-93](source/core/system/director.cpp#L64-L93)):
|
||||
|
||||
- **Release** (`RELEASE_BUILD`): `resources.pack` es **obligatorio** y se valida su
|
||||
integridad; si falla, el juego aborta. No hay fallback (ver memoria de proyecto
|
||||
*"No fallback a SDL_Renderer"* — aquí es la política equivalente para recursos).
|
||||
- **Dev**: intenta el pack; si no está, hace **fallback al directorio `data/`** del
|
||||
filesystem, escaneándolo según prefijo (`music/`, `sounds/`, `shapes/`).
|
||||
|
||||
El formato de datos de juego:
|
||||
|
||||
- **Entidades** (`data/entities/<nombre>/<nombre>.yaml`) — YAML declarativo con
|
||||
`shape`, `physics`, `ai`, `animation`, `wounded`, `spawn`, `colors`, `score`,
|
||||
`events`. Ejemplo: [data/entities/square/square.yaml](data/entities/square/square.yaml).
|
||||
- **Stages** (`data/stages/stages.yaml`) — oleadas (`waves`) con `spawn`,
|
||||
`spawn_interval`, `next` y multiplicadores de dificultad por stage.
|
||||
- **Shapes** (`data/shapes/**/*.shp`) — figuras vectoriales (ver [§5.2](#52-una-shape-y-cómo-se-carga)).
|
||||
|
||||
El parser YAML usado es [fkyaml](source/external/fkyaml_node.hpp) (cabecera única),
|
||||
envuelto por [config_yaml](source/game/config_yaml.hpp).
|
||||
|
||||
---
|
||||
|
||||
## 9. Comunicación entre módulos
|
||||
|
||||
No hay un sistema de mensajería desacoplado. La comunicación es:
|
||||
|
||||
1. **Eventos SDL → cadena del Director.** Por cada `SDL_Event`,
|
||||
[Director::handleEvent](source/core/system/director.cpp#L354) intenta, en orden:
|
||||
`SDLManager::handleWindowEvent` → `GlobalEvents::handle` → F11 (debug overlay) →
|
||||
`current_scene_->handleEvent`.
|
||||
|
||||
2. **GlobalEvents** ([global_events.cpp](source/core/system/global_events.cpp)) es
|
||||
el orquestador de la entrada global. Su `handle()` hace, en orden:
|
||||
`Input::get()->handleEvent` (hotplug) → `consumeIfDefineActive` (si el modal de
|
||||
rebinding está activo, **engulle todo**) → `SDL_EVENT_QUIT` → ratón → botón MENU
|
||||
del mando → reenvío al `ServiceMenu` si está abierto → teclas de función:
|
||||
|
||||
| Tecla | Acción |
|
||||
|---|---|
|
||||
| F1 / F2 | reducir / aumentar tamaño de ventana |
|
||||
| F3 | fullscreen |
|
||||
| F4 | VSync |
|
||||
| F5 | antialias geométrico |
|
||||
| F6 | post-procesado (bloom/flicker/fondo) |
|
||||
| F7 | idioma ca ↔ en (hot-swap de `Locale`) |
|
||||
| F11 | debug overlay (gestionado en el Director, no en GlobalEvents) |
|
||||
| F12 | menú de servicio |
|
||||
| ESC | doble pulsación para salir (la 1ª muestra un toast de confirmación) |
|
||||
|
||||
3. **Singletons compartidos.** `Input`, `Audio`, `Locale`, `Notifier`,
|
||||
`ServiceMenu`, `DefineInputs` se acceden globalmente vía `::get()`. Muchos
|
||||
comprueban `nullptr` para degradar con elegancia (p. ej. el hotplug notifica
|
||||
solo si `Notifier::get() != nullptr`).
|
||||
|
||||
4. **Paso por referencia.** Las escenas reciben `SDLManager&` y `SceneContext&`; el
|
||||
render se propaga como `Rendering::Renderer*`. Los sistemas de juego reciben un
|
||||
struct `Context` con punteros a los pools (ver [§10](#10-lógica-del-juego)).
|
||||
|
||||
**Overlays de sistema** (todos singletons, todos por encima de la escena):
|
||||
|
||||
- **[Notifier](source/core/system/notifier.hpp)** — toasts deslizantes centrados
|
||||
(`notifyInfo/Warn/Exit`), con máquina de animación HIDDEN/ENTERING/HOLDING/EXITING.
|
||||
- **[ServiceMenu](source/core/system/service_menu.hpp)** — menú de configuración
|
||||
(F12) con pila de páginas (vídeo, audio, controles, sistema...).
|
||||
- **[DebugOverlay](source/core/system/debug_overlay.hpp)** — HUD de FPS/VSync (F11).
|
||||
- **[Relaunch](source/core/system/relaunch.hpp)** — reinicio en caliente vía
|
||||
`execv` (lo solicita el ServiceMenu, lo ejecuta `SDL_AppQuit`).
|
||||
|
||||
**Lo que NO existe** (verificado): no hay event bus genérico, ni cola de mensajes
|
||||
desacoplada, ni un FSM genérico reutilizable fuera de las máquinas de estado
|
||||
concretas de cada escena/sistema, ni un ECS.
|
||||
|
||||
---
|
||||
|
||||
## 10. Lógica del juego
|
||||
|
||||
Toda la partida vive en [GameScene](source/game/scenes/game_scene.hpp). Es la clase
|
||||
más grande del juego y actúa como orquestador. Posee:
|
||||
|
||||
- El mundo físico [Physics::PhysicsWorld](source/core/physics/physics_world.hpp)
|
||||
(integración cinemática + colisiones físicas).
|
||||
- Pools de tamaño **fijo**: `std::array<Ship, 2>`,
|
||||
`std::array<Enemy, MAX_ORNIS>` (15), `std::array<Bullet, MAX_BULLETS_TOTAL>` (6:
|
||||
P1=[0,1,2], P2=[3,4,5]).
|
||||
- Estado de partida: vidas, score y *death timers* por jugador, máquina de
|
||||
game over (`GameOverState`: `NONE/CONTINUE/GAME_OVER`), continues usados.
|
||||
- El stage system, los efectos visuales, y los `DemoPilot` (uno por nave).
|
||||
|
||||
### 10.1 Orquestación por frame
|
||||
|
||||
[GameScene::update()](source/game/scenes/game_scene.cpp) es un orquestador delgado;
|
||||
cada paso es una función privada (descompuesto para reducir complejidad cognitiva):
|
||||
|
||||
```cpp
|
||||
void GameScene::update(float dt) {
|
||||
if (ServiceMenu abierto) return; // pausa global (draw sí sigue)
|
||||
stepPhysics(dt);
|
||||
if (mode == DEMO) { if (stepDemo(dt)) return; }
|
||||
else if (game_over_state_ == NONE) { stepShootingInput(); stepMidGameJoin(); }
|
||||
if (stepContinueScreen(dt)) return;
|
||||
if (stepGameOver(dt)) return;
|
||||
stepDeathSequence(dt);
|
||||
stepStageStateMachine(dt);
|
||||
}
|
||||
```
|
||||
|
||||
El corazón del gameplay es
|
||||
[stepStageStateMachine](source/game/scenes/game_scene.hpp#L166), que despacha según
|
||||
el estado del stage; en `PLAYING`,
|
||||
[runStagePlaying](source/game/scenes/game_scene.hpp#L169) ejecuta: WaveRunner
|
||||
(spawns) → IA de cada enemigo → control de naves
|
||||
([updateShipsControl](source/game/scenes/game_scene.cpp), que en demo usa
|
||||
`applyMovement` con el control del pilot y fuera de demo usa `processInput`) →
|
||||
detección de colisiones ([runCollisionDetections](source/game/scenes/game_scene.hpp#L176)).
|
||||
|
||||
`draw()` despacha de forma análoga según `GameOverState` y el estado del stage, y
|
||||
siempre pinta la cortinilla al final.
|
||||
|
||||
### 10.2 Entidades
|
||||
|
||||
Las tres heredan de `Entities::Entity` ([entity.hpp](source/core/entities/entity.hpp)):
|
||||
|
||||
- **[Ship](source/game/entities/ship.hpp)** — nave del jugador. `processInput()`
|
||||
(humano) y `applyMovement()` (usado por la IA demo). Estados: activa,
|
||||
invulnerable (parpadeo tras spawn), herida (`hurt`). Al morir genera debris con
|
||||
la inercia heredada.
|
||||
- **[Enemy](source/game/entities/enemy.hpp)** — 5 tipos (`EnemyType`: `PENTAGON`,
|
||||
`SQUARE`, `PINWHEEL`, `STAR`, `ORB`). Toda su config (físicas, IA, animación,
|
||||
eventos) viene del **YAML** vía [EnemyRegistry](source/game/entities/enemy_registry.hpp).
|
||||
Tiene salud (la mayoría HP=1; `ORB` HP=10) y estado *wounded* (parpadeo).
|
||||
- **[Bullet](source/game/entities/bullet.hpp)** — con `owner_id` (0=P1, 1=P2,
|
||||
≥16=enemigo) y `prev_position` para colisión *swept* (la bala que cruza un enemigo
|
||||
entre dos frames). Config en [BulletRegistry](source/game/entities/bullet_registry.hpp).
|
||||
|
||||
### 10.3 IA de enemigos: declarativa
|
||||
|
||||
Los enemigos **no** tienen comportamiento hardcoded. El YAML describe:
|
||||
|
||||
- Una **primitiva de movimiento** (`MovementType` en
|
||||
[enemy_ai.hpp](source/game/entities/enemy_ai.hpp)): `ZIGZAG`, `TRACKING`,
|
||||
`RECTILINEAR_PROXIMITY`, `WANDER`, `CHASE`, `FLEE`.
|
||||
- **Acciones de tick** periódicas (p. ej. `SHOOT`).
|
||||
- **Eventos** (`on_hit`, `on_no_health`, `on_hurt_end`, `on_destroy`) con acciones
|
||||
(`APPLY_IMPULSE`, `DECREASE_HEALTH`, `CREATE_DEBRIS`, `ADD_SCORE`, `FLASH`,
|
||||
`FIRE_BULLET`, `DESTROY`, ...).
|
||||
|
||||
Dos sistemas los ejecutan:
|
||||
|
||||
- **[EnemyAiSystem](source/game/systems/enemy_ai_system.hpp)** — `move()` aplica la
|
||||
primitiva de movimiento; `tick()` añade las acciones periódicas. Helper
|
||||
`findNearestShipPosition()` para las primitivas que buscan al jugador.
|
||||
- **[EnemyEventDispatcher](source/game/systems/enemy_event_dispatcher.hpp)** —
|
||||
ejecuta las acciones declarativas cuando se dispara un evento.
|
||||
|
||||
### 10.4 Colisiones
|
||||
|
||||
[CollisionSystem](source/game/systems/collision_system.hpp) recibe un struct
|
||||
`Context` (punteros a ships/enemies/bullets, managers de efectos, timers, scores,
|
||||
vidas y un callback `on_player_hit`) que GameScene construye en
|
||||
[buildCollisionContext](source/game/scenes/game_scene.hpp#L174). Detecta:
|
||||
bala↔enemigo, nave↔enemigo, bala↔jugador (fuego amigo / autodisparo), bala
|
||||
enemiga↔nave, y balas fuera del área. Reglas observadas: el primer impacto deja al
|
||||
enemigo *wounded*; el segundo lo destruye y suma score. La nave entra en `hurt` al
|
||||
primer toque y muere al segundo durante ese estado.
|
||||
|
||||
### 10.5 Stages y oleadas
|
||||
|
||||
- **[StageManager](source/game/stage_system/stage_manager.hpp)** — FSM del stage
|
||||
(`EstatStage`): `INIT_HUD` (anima el HUD, 3 s) → `LEVEL_START` ("ENEMY INCOMING",
|
||||
3 s, arranca `game.ogg`) → `PLAYING` → `LEVEL_COMPLETED` ("GOOD JOB COMMANDER!",
|
||||
3 s) → siguiente stage. `initDemo(stage_id)` arranca directamente en `PLAYING`
|
||||
para el attract mode.
|
||||
- **[WaveRunner](source/game/stage_system/wave_runner.hpp)** — emite los enemigos de
|
||||
cada oleada según `spawn_interval` y avanza cuando se cumple `next` (`all_dead`,
|
||||
`timeout`, o ambos).
|
||||
- **[StageConfig](source/game/stage_system/stage_config.hpp)** /
|
||||
[StageLoader](source/game/stage_system/stage_loader.hpp) — modelo y carga del
|
||||
YAML de stages.
|
||||
|
||||
### 10.6 Dos capas de colisión: física vs gameplay
|
||||
|
||||
Conviene no confundirlas, porque conviven:
|
||||
|
||||
**1. Física** — [PhysicsWorld](source/core/physics/physics_world.hpp) /
|
||||
[physics_world.cpp](source/core/physics/physics_world.cpp). Es un mundo 2D
|
||||
minimalista de arcade. Cada frame, `update(dt)` hace tres pasos:
|
||||
|
||||
1. **Integración** semi-implícita de Euler con damping exponencial
|
||||
(`v += (F·invMass)·dt; v *= exp(-damping·dt); x += v·dt`) sobre cada
|
||||
[RigidBody](source/core/physics/rigid_body.hpp) no estático. Un cuerpo con
|
||||
`mass=0` (`inverse_mass=0`) es estático (masa infinita).
|
||||
2. **Rebote contra los bordes** del `PLAYAREA` (`resolveBoundsCollisions`): reposiciona
|
||||
el cuerpo dentro del rect y refleja la componente normal de la velocidad por su
|
||||
`restitution`. Antes de reflejar, invoca un `BoundsHitCallback` opcional con la
|
||||
velocidad de impacto entrante (lo usa GameScene para los efectos de borde).
|
||||
3. **Colisiones cuerpo-cuerpo** (`resolveBodyCollisions`): broadphase trivial
|
||||
**O(n²)** (suficiente para ~23 cuerpos), círculo-círculo, con corrección posicional
|
||||
de penetración + **impulso elástico** `j = -(1+e)(v_rel·n) / (1/mₐ + 1/m_b)`
|
||||
(referencia Box2D / Chris Hecker, en `resolveBodyPair`). Los cuerpos con `radius=0`
|
||||
(las balas, cinemáticas puras) **no** participan aquí.
|
||||
|
||||
Los `RigidBody` los poseen las entidades; el mundo solo guarda punteros no-owning
|
||||
(`addBody`/`removeBody`).
|
||||
|
||||
**2. Gameplay** — [collision_system.cpp](source/game/systems/collision_system.cpp)
|
||||
(ver [§10.4](#104-colisiones)), que decide *qué pasa* (daño, score, muerte). Usa los
|
||||
helpers de [collision.hpp](source/core/physics/collision.hpp): `checkCollision`
|
||||
(círculo-círculo discreto, distancia al cuadrado sin `sqrt`) y `checkCollisionSwept`
|
||||
(segment-círculo, para que una bala rápida no atraviese un enemigo entre frames —
|
||||
*anti-tunneling*). Estos checks usan el `collision_radius` de la **entidad**
|
||||
(con amplificador opcional de hitbox), no el `radius` del body.
|
||||
|
||||
En resumen: la **física** mueve y rebota los cuerpos; el **gameplay** detecta los
|
||||
contactos relevantes para las reglas. Una bala no rebota físicamente (radius 0) pero sí
|
||||
provoca daño vía el check *swept*.
|
||||
|
||||
---
|
||||
|
||||
## 11. IA del modo demo (attract)
|
||||
|
||||
El attract mode es una partida que se juega sola para atraer al jugador. Se activa
|
||||
desde [TitleScene](source/game/scenes/title_scene.hpp) cuando el `idle_timer_` en el
|
||||
estado `MAIN` supera el umbral de inactividad, y desde
|
||||
[GameScene](source/game/scenes/game_scene.hpp) cuando `match_config_.mode == DEMO`.
|
||||
|
||||
La IA vive en [DemoPilot](source/game/systems/demo_pilot.hpp) /
|
||||
[demo_pilot.cpp](source/game/systems/demo_pilot.cpp). Su diseño es explícito en la
|
||||
cabecera: busca **parecer humano, no ser óptimo**. Características clave:
|
||||
|
||||
- **Solo lectura**: `DemoPilot::compute(ship, enemies, bullets, play_area, dt)`
|
||||
devuelve un `Control{left,right,thrust,shoot}`. No lee `Input` ni muta entidades;
|
||||
GameScene aplica el resultado vía `Ship::applyMovement` + `fireBullet`.
|
||||
- **Escenarios curados**: hay 4 (`SCENARIOS` en
|
||||
[demo_pilot.hpp:36-42](source/game/systems/demo_pilot.hpp#L36-L42)): stages
|
||||
`{5,8,6,10}` con 1 o 2 naves IA. El `SceneContext` recuerda el índice y rota al
|
||||
siguiente en cada entrada al demo.
|
||||
|
||||
**Lógica de decisión por prioridad** (verificado en `demo_pilot.cpp`, con sus
|
||||
constantes):
|
||||
|
||||
1. **Esquiva de bala** — si una bala enemiga entrante está dentro de
|
||||
`DODGE_SCAN_RADIUS = 190 px` y viene hacia la nave (`DODGE_HEADING_MIN = 0.25`),
|
||||
maniobra perpendicular a la bala con sesgo al centro (`WALL_BIAS = 0.6`); no
|
||||
dispara mientras esquiva.
|
||||
2. **Sin enemigos** — deriva tranquila (giro lento).
|
||||
3. **Peligro cercano** — si el objetivo está a menos de `DANGER_RADIUS = 95 px`, se
|
||||
aleja con sesgo al centro.
|
||||
4. **Combate** — apuntado con *lead* (`LEAD_TIME = 0.30 s`) más un error humano
|
||||
(`AIM_JITTER_MAX = 0.10 rad`); dispara si el error es menor que
|
||||
`FIRE_TOLERANCE = 0.18 rad` y el cooldown (`FIRE_COOLDOWN = 0.32 s`) lo permite;
|
||||
se acerca si está más lejos que `APPROACH_RADIUS = 250 px`.
|
||||
|
||||
Temporización "humana": reevalúa el objetivo cada `RETARGET_INTERVAL = 0.15 s` y usa
|
||||
una zona muerta de rotación (`ROTATE_DEADZONE = 0.05 rad`) para no oscilar. La demo
|
||||
se rompe con cualquier input (vuelve a TITLE) o por timeout/muerte (vuelve a LOGO),
|
||||
gestionado en [stepDemo](source/game/scenes/game_scene.hpp#L157).
|
||||
|
||||
---
|
||||
|
||||
## 12. Efectos visuales
|
||||
|
||||
Viven en [game/effects/](source/game/effects/) y son managers con pools:
|
||||
|
||||
- **[DebrisManager](source/game/effects/debris_manager.hpp)** — rompe una shape en
|
||||
fragmentos que vuelan radialmente, heredando inercia del cuerpo y, opcionalmente,
|
||||
el impulso de la bala que causó la muerte. Notifica al `Border` (bump) y al
|
||||
`Playfield` (ripple). Lo usan muerte de nave/enemigo, balas fuera de área y las
|
||||
explosiones del logo.
|
||||
- **[FireworkManager](source/game/effects/firework_manager.hpp)** — bursts de fuegos
|
||||
artificiales.
|
||||
- **[FloatingScoreManager](source/game/effects/floating_score_manager.hpp)** —
|
||||
números de puntuación flotantes ("+150").
|
||||
- **[TrailManager](source/game/effects/trail_manager.hpp)** — estela tras las naves.
|
||||
|
||||
---
|
||||
|
||||
## 13. Configuración, constantes y convenciones
|
||||
|
||||
**Configuración:**
|
||||
|
||||
- **[EngineConfig](source/core/config/engine_config.hpp)** — struct POD con
|
||||
ventana, rendering, audio, bindings de jugadores, locale, console. Es la config
|
||||
persistente (`config.yaml`), gestionada por
|
||||
[config_yaml](source/game/config_yaml.hpp) (`ConfigYaml::engine_config`,
|
||||
`loadFromFile`/`saveToFile`).
|
||||
- **[PostFxConfig](source/core/config/postfx_config.hpp)** — carga los `PostFxParams`
|
||||
(bloom/flicker/fondo) desde YAML.
|
||||
- **[GameConfig::MatchConfig](source/core/system/game_config.hpp)** — config no
|
||||
persistente de la partida (jugadores activos, modo NORMAL/DEMO).
|
||||
|
||||
**Constantes y tipos:**
|
||||
|
||||
- **[core/types.hpp](source/core/types.hpp)** — `Vec2` / `Vec3` (agregados con
|
||||
operadores y helpers como `length()`, `normalized()`, `dot()`, `cross()`).
|
||||
- **[core/defaults/](source/core/defaults/)** — un fichero por dominio
|
||||
(`window.hpp`, `rendering.hpp`, `audio.hpp`, `entities.hpp`, `notifier.hpp`...)
|
||||
con todas las constantes por defecto. `game/constants.hpp` reexporta varias como
|
||||
alias (`MAX_ORNIS`, `MAX_BULLETS`, `PI`) y añade helpers de área de juego.
|
||||
|
||||
**Convenciones de código** (de `.clang-tidy`, confirmadas en memoria de proyecto):
|
||||
|
||||
- Métodos en `camelBack`, tipos en `CamelCase`, constantes en `UPPER_CASE`.
|
||||
- Comentarios mayormente en **catalán** (algunos en castellano); el código y los
|
||||
identificadores mezclan catalán/castellano/inglés.
|
||||
- Patrón recurrente: **singletons** con `init/get/destroy` y comprobación de
|
||||
`nullptr` para degradación elegante.
|
||||
- Patrón recurrente: descomposición de funciones grandes (`update`/`draw`) en
|
||||
sub-pasos privados (`stepX`/`runX`/`drawXState`) para mantener baja la complejidad
|
||||
cognitiva.
|
||||
- Análisis estático (cppcheck/clang-tidy) corre vía git hooks
|
||||
([.githooks/](.githooks/)); la política es **arreglar la causa**, no suprimir el
|
||||
diagnóstico.
|
||||
|
||||
---
|
||||
|
||||
## 14. Guía de navegación
|
||||
|
||||
| Si quieres tocar… | Mira… |
|
||||
|---|---|
|
||||
| El arranque, orden de init, o el bucle de frame | [director.cpp](source/core/system/director.cpp) (`Director::iterate` / `handleEvent`) |
|
||||
| Las callbacks de SDL | [main.cpp](source/main.cpp) |
|
||||
| Añadir/cambiar una escena o una transición | [scene.hpp](source/core/system/scene.hpp), [scene_context.hpp](source/core/system/scene_context.hpp), `Director::buildScene` |
|
||||
| Cómo se dibuja una línea / el frame de render | [line_renderer.cpp](source/core/rendering/line_renderer.cpp) → [gpu_frame_renderer.cpp](source/core/rendering/gpu/gpu_frame_renderer.cpp) |
|
||||
| Bloom / flicker / fondo (post-proceso) | [gpu_postfx_pipeline](source/core/rendering/gpu/gpu_postfx_pipeline.hpp), [gpu_bloom_pipeline](source/core/rendering/gpu/gpu_bloom_pipeline.hpp), shaders en [shaders/](shaders/) |
|
||||
| Crear/editar una figura vectorial | `data/shapes/**/*.shp` + [shape_loader.hpp](source/core/graphics/shape_loader.hpp) |
|
||||
| El texto en pantalla | [vector_text.hpp](source/core/graphics/vector_text.hpp) |
|
||||
| Eventos globales (teclas F, ESC, hotplug) | [global_events.cpp](source/core/system/global_events.cpp) |
|
||||
| Controles, bindings, rebinding | [input.cpp](source/core/input/input.cpp), [define_inputs.cpp](source/core/input/define_inputs.cpp) |
|
||||
| Reproducir música/efectos | [audio.hpp](source/core/audio/audio.hpp), [audio_adapter.hpp](source/core/audio/audio_adapter.hpp) |
|
||||
| Cómo se cargan los recursos / el pack | [resource_loader.cpp](source/core/resources/resource_loader.cpp), [resource_pack.cpp](source/core/resources/resource_pack.cpp) |
|
||||
| Reglas de la partida, vidas, game over | [game_scene.cpp](source/game/scenes/game_scene.cpp) |
|
||||
| Comportamiento de un enemigo | su YAML en `data/entities/<tipo>/` + [enemy_ai_system.cpp](source/game/systems/enemy_ai_system.cpp) |
|
||||
| Definir oleadas / dificultad de un nivel | [data/stages/stages.yaml](data/stages/stages.yaml) + [stage_manager.cpp](source/game/stage_system/stage_manager.cpp) |
|
||||
| Colisiones | [collision_system.cpp](source/game/systems/collision_system.cpp) |
|
||||
| La IA del modo demo | [demo_pilot.cpp](source/game/systems/demo_pilot.cpp) |
|
||||
| Explosiones / partículas | [debris_manager.cpp](source/game/effects/debris_manager.cpp) |
|
||||
| El menú de servicio (F12) | [service_menu.cpp](source/core/system/service_menu.cpp) |
|
||||
| Textos traducibles | `data/locale/*.yaml` + [locale.cpp](source/core/locale/locale.cpp) |
|
||||
| Constantes por defecto | [core/defaults/](source/core/defaults/) |
|
||||
|
||||
---
|
||||
|
||||
### Notas de honestidad sobre la cobertura
|
||||
|
||||
- Todas las secciones se verificaron leyendo directamente los ficheros y firmas
|
||||
citados, incluyendo el **pipeline de compilación de shaders**
|
||||
([§5.5](#55-shaders-fuentes-compilación-y-selección): `CMakeLists.txt` +
|
||||
`tools/shaders/compile_spirv.cmake` + `shader_factory.hpp`) y el interior de la
|
||||
**física** ([§10.6](#106-dos-capas-de-colisión-física-vs-gameplay):
|
||||
`physics_world.cpp` + `collision.hpp` + `rigid_body.hpp`).
|
||||
- Lo que **no** se ha trazado a fondo y queda como lectura directa del código si hace
|
||||
falta: los detalles finos de animación de cada overlay (curvas de easing del
|
||||
`Notifier`/`ServiceMenu`) y la coreografía interna completa de `LogoScene` y
|
||||
`TitleScene` (más allá de sus estados). Son descriptivos, no estructurales.
|
||||
</content>
|
||||
</invoke>
|
||||
@@ -84,9 +84,18 @@ else
|
||||
endif
|
||||
|
||||
.PHONY: all debug release _windows-release _macos-release _linux-release \
|
||||
run run-debug clean rebuild show-version pack \
|
||||
run run-debug clean rebuild show-version pack controllerdb \
|
||||
format format-check tidy tidy-fix cppcheck hooks-install help
|
||||
|
||||
# Còpia del gamecontrollerdb.txt (si existeix) al directori de build, perquè
|
||||
# director.cpp el resolgui via resource_base = directori de l'executable.
|
||||
# Silenciós si el fitxer no existeix (l'usuari encara no ha fet `make controllerdb`).
|
||||
ifeq ($(OS),Windows_NT)
|
||||
CP_CONTROLLERDB = @powershell -Command "if (Test-Path 'gamecontrollerdb.txt') { Copy-Item 'gamecontrollerdb.txt' -Destination '$(BUILDDIR)' -Force }"
|
||||
else
|
||||
CP_CONTROLLERDB = @if [ -f gamecontrollerdb.txt ]; then cp gamecontrollerdb.txt $(BUILDDIR)/; fi
|
||||
endif
|
||||
|
||||
# ==============================================================================
|
||||
# COMPILACIÓ
|
||||
# ==============================================================================
|
||||
@@ -98,10 +107,12 @@ endif
|
||||
all:
|
||||
@cmake -S . -B $(BUILDDIR) $(CMAKE_GEN) -DCMAKE_BUILD_TYPE=Release $(CMAKE_DEFS)
|
||||
@cmake --build $(BUILDDIR) -j$(JOBS)
|
||||
$(CP_CONTROLLERDB)
|
||||
|
||||
debug:
|
||||
@cmake -S . -B $(BUILDDIR) $(CMAKE_GEN) -DCMAKE_BUILD_TYPE=Debug $(CMAKE_DEFS)
|
||||
@cmake --build $(BUILDDIR) -j$(JOBS)
|
||||
$(CP_CONTROLLERDB)
|
||||
|
||||
run: all
|
||||
@./$(BUILDDIR)/$(PROJECT)
|
||||
@@ -138,6 +149,7 @@ _linux-release:
|
||||
|
||||
# Còpia de fitxers
|
||||
cp $(BUILDDIR)/resources.pack "$(RELEASE_FOLDER)"
|
||||
cp gamecontrollerdb.txt "$(RELEASE_FOLDER)"
|
||||
cp README.md "$(RELEASE_FOLDER)"
|
||||
@[ -f LICENSE ] && cp LICENSE "$(RELEASE_FOLDER)" || true
|
||||
cp "$(TARGET_FILE)" "$(RELEASE_FILE)"
|
||||
@@ -166,6 +178,7 @@ _windows-release:
|
||||
@powershell -Command "if (-not (Test-Path '$(RELEASE_FOLDER)')) {New-Item '$(RELEASE_FOLDER)' -ItemType Directory}"
|
||||
|
||||
@powershell -Command "Copy-Item -Path '$(BUILDDIR)/resources.pack' -Destination '$(RELEASE_FOLDER)'"
|
||||
@powershell -Command "Copy-Item 'gamecontrollerdb.txt' -Destination '$(RELEASE_FOLDER)'"
|
||||
@powershell -Command "if (Test-Path 'LICENSE') { Copy-Item 'LICENSE' -Destination '$(RELEASE_FOLDER)' }"
|
||||
@powershell -Command "Copy-Item 'README.md' -Destination '$(RELEASE_FOLDER)'"
|
||||
@powershell -Command "if (Test-Path 'release\windows\dll') { Copy-Item 'release\windows\dll\*.dll' -Destination '$(RELEASE_FOLDER)' }"
|
||||
@@ -189,7 +202,7 @@ _macos-release:
|
||||
|
||||
# Compila la versió Apple Silicon
|
||||
@cmake -S . -B $(BUILDDIR)/arm $(CMAKE_GEN) -DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_OSX_ARCHITECTURES=arm64 -DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 \
|
||||
-DCMAKE_OSX_ARCHITECTURES=arm64 -DCMAKE_OSX_DEPLOYMENT_TARGET=13.3 \
|
||||
-DMACOS_BUNDLE=ON $(CMAKE_DEFS)
|
||||
@cmake --build $(BUILDDIR)/arm -j$(JOBS)
|
||||
|
||||
@@ -206,6 +219,7 @@ _macos-release:
|
||||
|
||||
# Còpia de recursos i metadades del bundle
|
||||
cp $(BUILDDIR)/arm/resources.pack "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
|
||||
cp gamecontrollerdb.txt "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
|
||||
cp -R release/macos/frameworks/SDL3.xcframework/macos-arm64_x86_64/SDL3.framework "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Frameworks"
|
||||
cp release/icons/icon.icns "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
|
||||
cp release/macos/Info.plist "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents"
|
||||
@@ -274,6 +288,19 @@ pack:
|
||||
@cmake --build $(BUILDDIR) --target pack_resources
|
||||
@./$(BUILDDIR)/pack_resources data $(BUILDDIR)/resources.pack
|
||||
|
||||
# ==============================================================================
|
||||
# DESCÀRREGA DE GAMECONTROLLERDB
|
||||
# ==============================================================================
|
||||
# Descarrega l'última versió de gamecontrollerdb.txt (mappings de gamepads
|
||||
# mantinguts per la comunitat) a l'arrel del projecte. SDL el carrega via
|
||||
# filesystem real (no dins resources.pack) i s'ha de copiar al costat del binari
|
||||
# en cada build (gestionat per CP_CONTROLLERDB a `all`/`debug` i pels release targets).
|
||||
controllerdb:
|
||||
@echo "Descargando gamecontrollerdb.txt..."
|
||||
curl -fsSL https://raw.githubusercontent.com/mdqinc/SDL_GameControllerDB/master/gamecontrollerdb.txt \
|
||||
-o gamecontrollerdb.txt
|
||||
@echo "gamecontrollerdb.txt actualizado"
|
||||
|
||||
# ==============================================================================
|
||||
# CODE QUALITY (delegats a cmake)
|
||||
# ==============================================================================
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
name: bullet
|
||||
|
||||
# Shape de la bala. El bounding_radius del .shp dóna el hitbox base (~3 px);
|
||||
# scale el modula visualment i pel hitbox.
|
||||
shape:
|
||||
path: bullet/basic.shp
|
||||
scale: 1.0
|
||||
collision_factor: 1.0
|
||||
|
||||
# Cinemàtica pura: la bala no col·lisiona físicament al PhysicsWorld
|
||||
# (body_.radius = 0 al spawn), però sí participa al gameplay via
|
||||
# checkCollisionSwept. La mass i l'impact_momentum_factor es fan servir
|
||||
# només per calcular l'impuls que rep l'enemic en impactar.
|
||||
physics:
|
||||
mass: 0.5
|
||||
restitution: 0.0 # irrelevant (no rebota)
|
||||
linear_damping: 0.0 # movement rectilini uniforme
|
||||
angular_damping: 0.0
|
||||
impact_momentum_factor: 3.0 # factor de transferència de moment bala→enemic
|
||||
|
||||
colors:
|
||||
normal: [155, 255, 175] # verd laser
|
||||
@@ -0,0 +1,21 @@
|
||||
name: bullet_double
|
||||
|
||||
# Variant de bala "anular" (dos cercles concèntrics, aspecte d'aura de plasma).
|
||||
# Pensada per a contra-atacs d'enemic (ex: orb dispara una bullet_double al
|
||||
# jugador quan rep un impacte). Mateixa física que la bala bàsica del player;
|
||||
# canvien la forma (cercle doble) i el color per llegir-se com a tret enemic
|
||||
# distintiu (groc verdós vs. el verd laser del player o el roig de bullet_long).
|
||||
shape:
|
||||
path: bullet/double.shp
|
||||
scale: 1.5
|
||||
collision_factor: 1.0
|
||||
|
||||
physics:
|
||||
mass: 0.5
|
||||
restitution: 0.0
|
||||
linear_damping: 0.0
|
||||
angular_damping: 0.0
|
||||
impact_momentum_factor: 4.0
|
||||
|
||||
colors:
|
||||
normal: [200, 255, 80] # groc verdós (chartreuse) — contra-atac de l'orb
|
||||
@@ -0,0 +1,19 @@
|
||||
name: bullet_long
|
||||
|
||||
# Variant de bala més llarga, pensada per a bales d'enemic: més visible per al
|
||||
# jugador i amb prou marge per reaccionar. La velocitat NO viu aquí: es passa
|
||||
# a Bullet::fire() i la decideix qui dispara (l'AiTickAction).
|
||||
shape:
|
||||
path: bullet/long.shp
|
||||
scale: 1.0
|
||||
collision_factor: 0.5
|
||||
|
||||
physics:
|
||||
mass: 0.5
|
||||
restitution: 0.0
|
||||
linear_damping: 0.0
|
||||
angular_damping: 0.0
|
||||
impact_momentum_factor: 3.0
|
||||
|
||||
colors:
|
||||
normal: [255, 100, 100] # roig clar — diferencia visualment del verd laser del player
|
||||
@@ -0,0 +1,86 @@
|
||||
name: orb
|
||||
ai_type: orb # Validat contra el directori; mapeja a EnemyType::ORB.
|
||||
|
||||
# Shape circular pròpia (anell exterior + anell interior + 6 radis + nucli),
|
||||
# pensada per llegir-se com a "reactor / orb" amb més detall que els enemics
|
||||
# petits.
|
||||
shape:
|
||||
path: enemy/orb.shp
|
||||
scale: 1.0
|
||||
collision_factor: 1.0
|
||||
|
||||
physics:
|
||||
mass: 50.0 # Molt pesat: una bala el frena un poc però no el "envia a passejar".
|
||||
speed: 50.0 # Avança decidit cap al ship (no és lent passiu, és amenaça constant).
|
||||
rotation_delta_min: 0.3
|
||||
rotation_delta_max: 1.5
|
||||
restitution: 1.0
|
||||
linear_damping: 0.0
|
||||
angular_damping: 0.0
|
||||
|
||||
ai:
|
||||
# Persecució contínua del ship més proper. chase_strength alt (1.0 = ~1s
|
||||
# per realinear-se) perquè, encara que una bala l'empentja lateralment,
|
||||
# ràpidament torna a posar la seua proa cap al jugador.
|
||||
movement:
|
||||
type: chase
|
||||
chase_strength: 1.0
|
||||
|
||||
animation:
|
||||
pulse:
|
||||
trigger_prob_per_second: 0.01
|
||||
duration_min: 1.0
|
||||
duration_max: 3.0
|
||||
amplitude_min: 0.08
|
||||
amplitude_max: 0.20
|
||||
frequency_min: 1.5
|
||||
frequency_max: 3.0
|
||||
rotation_accel:
|
||||
trigger_prob_per_second: 0.02
|
||||
duration_min: 3.0
|
||||
duration_max: 8.0
|
||||
multiplier_min: 0.3
|
||||
multiplier_max: 4.0
|
||||
|
||||
wounded:
|
||||
duration: 1.5 # Una mica més llarg que els altres (és un boss).
|
||||
blink_hz: 10.0
|
||||
|
||||
spawn:
|
||||
invulnerability_duration: 3.0
|
||||
invulnerability_brightness_start: 0.3
|
||||
invulnerability_brightness_end: 0.7
|
||||
invulnerability_scale_start: 0.0
|
||||
invulnerability_scale_end: 1.0
|
||||
safety_distance: 54.0 # 1.5× del normal (alineat amb scale 1.5).
|
||||
|
||||
colors:
|
||||
normal: [255, 140, 110] # taronja rosat (coral) — distintiu del boss orb.
|
||||
wounded: [255, 220, 60]
|
||||
|
||||
score: 500 # 5x un enemic normal: aguanta 10x més.
|
||||
|
||||
# Estrenant el sistema HP: 10 unitats. Cada bala fa decrease_health + flash
|
||||
# + create_debris_partial (xip a 0.3x) + create_fireworks_small (espurna).
|
||||
# Al 10è hit (HP=0), on_no_health encadena destroy directe — sense passar
|
||||
# per wounded (com Star). 10 HP ja és prou dificultat sense afegir un hit
|
||||
# extra.
|
||||
health: 10
|
||||
|
||||
events:
|
||||
on_hit:
|
||||
- action: fire_bullet # contra-atac: dispara bullet_double dirigida al jugador
|
||||
bullet: bullet_double
|
||||
bullet_speed: 200.0
|
||||
aim_mode: aimed
|
||||
- action: decrease_health # primer: si arriba a 0 dispara on_no_health
|
||||
#- action: flash # feedback visual de damage parcial
|
||||
- action: create_debris_partial # xip a 0.3x mida (sense ser letal)
|
||||
#- action: create_fireworks_small # espurna a cada hit (12 punts, lent)
|
||||
- action: apply_impulse # empenta el cos (skip si will_die)
|
||||
on_no_health:
|
||||
- action: destroy # mort directa, sense wounded
|
||||
on_destroy:
|
||||
- action: add_score
|
||||
- action: create_debris # explosió completa
|
||||
- action: create_fireworks
|
||||
@@ -0,0 +1,72 @@
|
||||
name: pentagon
|
||||
ai_type: pentagon # Validat contra el directori; mapeja a EnemyType::PENTAGON.
|
||||
|
||||
shape:
|
||||
path: enemy/pentagon.shp
|
||||
scale: 1.0 # multiplicador visual + hitbox sobre la mida nativa del .shp
|
||||
collision_factor: 1.0 # ajust opcional del hitbox (default 1.0)
|
||||
|
||||
physics:
|
||||
mass: 5.0
|
||||
speed: 35.0 # px/s (esquivador lent)
|
||||
rotation_delta_min: 0.75 # rad/s — rotació visual mínima
|
||||
rotation_delta_max: 3.75 # rad/s — rotació visual màxima
|
||||
restitution: 1.0 # rebot elàstic perfecte contra parets
|
||||
linear_damping: 0.0 # manté velocitat (sense fricció)
|
||||
angular_damping: 0.0
|
||||
|
||||
behavior:
|
||||
# Pentagon: zigzag esquivador (canvi de direcció probabilístic per segon).
|
||||
angle_change_max: 1.0 # rad — magnitud del canvi de direcció
|
||||
zigzag_prob_per_second: 0.8
|
||||
|
||||
animation:
|
||||
pulse: # respiració d'escala aleatòria
|
||||
trigger_prob_per_second: 0.01
|
||||
duration_min: 1.0
|
||||
duration_max: 3.0
|
||||
amplitude_min: 0.08
|
||||
amplitude_max: 0.20
|
||||
frequency_min: 1.5
|
||||
frequency_max: 3.0
|
||||
rotation_accel: # acceleració/desacceleració de rotació visual
|
||||
trigger_prob_per_second: 0.02
|
||||
duration_min: 3.0
|
||||
duration_max: 8.0
|
||||
multiplier_min: 0.3
|
||||
multiplier_max: 4.0
|
||||
|
||||
wounded:
|
||||
duration: 1.0 # segons en estat ferit abans d'explotar
|
||||
blink_hz: 10.0 # parpelleig color normal ↔ wounded
|
||||
|
||||
spawn:
|
||||
invulnerability_duration: 3.0
|
||||
invulnerability_brightness_start: 0.3
|
||||
invulnerability_brightness_end: 0.7
|
||||
invulnerability_scale_start: 0.0
|
||||
invulnerability_scale_end: 1.0
|
||||
safety_distance: 36.0 # px mínim respecte al player al spawn
|
||||
|
||||
colors:
|
||||
normal: [0, 255, 255] # Cyan pur "esquivador"
|
||||
wounded: [255, 220, 60] # Daurat (parpelleig al rebre impacte)
|
||||
|
||||
score: 100
|
||||
|
||||
events:
|
||||
# HP=1 (default): decrement → on_no_health → set_hurt → wounded → mort.
|
||||
# decrease_health primer perquè si la mort cau aquí (segon hit durant wounded),
|
||||
# el dispatcher salta la resta del chain (incloent apply_impulse) sobre el
|
||||
# cos ja destruït.
|
||||
on_hit:
|
||||
- action: decrease_health
|
||||
- action: apply_impulse
|
||||
on_no_health:
|
||||
- action: set_hurt
|
||||
on_hurt_end:
|
||||
- action: destroy
|
||||
on_destroy:
|
||||
- action: add_score
|
||||
- action: create_debris
|
||||
- action: create_fireworks
|
||||
@@ -0,0 +1,69 @@
|
||||
name: pinwheel
|
||||
ai_type: pinwheel # Validat contra el directori; mapeja a EnemyType::PINWHEEL.
|
||||
|
||||
shape:
|
||||
path: enemy/pinwheel.shp
|
||||
scale: 1.0 # multiplicador visual + hitbox sobre la mida nativa del .shp
|
||||
collision_factor: 1.0 # ajust opcional del hitbox (default 1.0)
|
||||
|
||||
physics:
|
||||
mass: 4.0 # Més lleuger — àgil
|
||||
speed: 50.0 # px/s (el més ràpid)
|
||||
rotation_delta_min: 3.0 # rad/s — rotació base elevada
|
||||
rotation_delta_max: 6.0
|
||||
restitution: 1.0
|
||||
linear_damping: 0.0
|
||||
angular_damping: 0.0
|
||||
|
||||
behavior:
|
||||
# Pinwheel: movement rectilíniauniforme + boost de rotació visual prop de la nau.
|
||||
rotation_proximity_multiplier: 3.0 # Multiplicador de rotació quan és prop de la nau
|
||||
proximity_distance: 100.0 # Llindar de distància (px)
|
||||
|
||||
animation:
|
||||
pulse:
|
||||
trigger_prob_per_second: 0.01
|
||||
duration_min: 1.0
|
||||
duration_max: 3.0
|
||||
amplitude_min: 0.08
|
||||
amplitude_max: 0.20
|
||||
frequency_min: 1.5
|
||||
frequency_max: 3.0
|
||||
rotation_accel:
|
||||
trigger_prob_per_second: 0.02
|
||||
duration_min: 3.0
|
||||
duration_max: 8.0
|
||||
multiplier_min: 0.3
|
||||
multiplier_max: 4.0
|
||||
|
||||
wounded:
|
||||
duration: 1.0
|
||||
blink_hz: 10.0
|
||||
|
||||
spawn:
|
||||
invulnerability_duration: 3.0
|
||||
invulnerability_brightness_start: 0.3
|
||||
invulnerability_brightness_end: 0.7
|
||||
invulnerability_scale_start: 0.0
|
||||
invulnerability_scale_end: 1.0
|
||||
safety_distance: 36.0
|
||||
|
||||
colors:
|
||||
normal: [255, 0, 255] # Magenta pur "agressiu"
|
||||
wounded: [255, 220, 60]
|
||||
|
||||
score: 200
|
||||
|
||||
events:
|
||||
# HP=1 (default): decrement → on_no_health → set_hurt → wounded → mort.
|
||||
on_hit:
|
||||
- action: decrease_health
|
||||
- action: apply_impulse
|
||||
on_no_health:
|
||||
- action: set_hurt
|
||||
on_hurt_end:
|
||||
- action: destroy
|
||||
on_destroy:
|
||||
- action: add_score
|
||||
- action: create_debris
|
||||
- action: create_fireworks
|
||||
@@ -0,0 +1,49 @@
|
||||
name: player_ship
|
||||
|
||||
# Shape de la nau. Resolt per ShapeLoader (busca a "shapes/<path>").
|
||||
# Nota: el segon jugador rep un override del shape ("ship/wedge.shp") al ctor.
|
||||
# Quan s'introdueixin variants reals de nau, es crearà un YAML separat
|
||||
# per cada model.
|
||||
#
|
||||
# scale: multiplicador visual i de hitbox sobre la mida nativa del .shp (1.0 = mida del fitxer).
|
||||
# collision_factor: ajust opcional del hitbox respecte el cercle circumscrit
|
||||
# automàtic de la shape; tocar només si el feel del hitbox
|
||||
# no quadra amb la silueta visual (default 1.0).
|
||||
shape:
|
||||
path: ship/arrow.shp
|
||||
scale: 1.0
|
||||
collision_factor: 1.0
|
||||
|
||||
physics:
|
||||
mass: 10.0
|
||||
restitution: 0.6
|
||||
linear_damping: 1.5
|
||||
angular_damping: 0.0
|
||||
rotation_speed: 3.14 # rad/s (~180 deg/s, input-driven sense inercia)
|
||||
acceleration: 400.0 # px/s^2 multiplicat per la massa quan THRUST
|
||||
max_velocity: 180.0 # px/s (clamp post-integració per preservar feel arcade)
|
||||
# Factor de transferència del moment lineal de la nau a l'enemic en el
|
||||
# frame exacte que mor per col·lisió (afegit per damunt del rebot natural).
|
||||
death_impact_factor: 0.3
|
||||
|
||||
invulnerability:
|
||||
duration: 3.0 # segons d'invulnerabilitat post-respawn
|
||||
blink_visible: 0.1 # segons visible per cicle de parpelleig
|
||||
blink_invisible: 0.1 # segons invisible per cicle de parpelleig
|
||||
|
||||
hurt:
|
||||
duration: 15.0 # segons en estat "ferit" abans de tornar a normal
|
||||
blink_hz: 10.0 # freqüència parpelleig color normal <-> color hurt
|
||||
|
||||
# Empenta visual: la nau s'escala lleugerament amb la velocitat.
|
||||
# Manté la sensació del Pascal original (0..MAX_VEL → 1.0..~1.5).
|
||||
visual_thrust:
|
||||
push_divisor: 33.33
|
||||
scale_divisor: 12.0
|
||||
|
||||
colors:
|
||||
normal: [255, 255, 255] # blanc neutre
|
||||
hurt: [255, 0, 0] # roig pur (estat ferit)
|
||||
|
||||
weapon:
|
||||
bullet_speed: 700.0 # velocitat escalar de la bullet (px/s)
|
||||
@@ -0,0 +1,70 @@
|
||||
name: square
|
||||
ai_type: square # Validat contra el directori; mapeja a EnemyType::SQUARE.
|
||||
|
||||
shape:
|
||||
path: enemy/square.shp
|
||||
scale: 1.0 # multiplicador visual + hitbox sobre la mida nativa del .shp
|
||||
collision_factor: 1.0 # ajust opcional del hitbox (default 1.0)
|
||||
|
||||
physics:
|
||||
mass: 8.0 # Més pesat — "tanc"
|
||||
speed: 40.0 # px/s (velocitat mitjana)
|
||||
rotation_delta_min: 0.3 # rad/s — rotació lenta
|
||||
rotation_delta_max: 1.5
|
||||
restitution: 1.0
|
||||
linear_damping: 0.0
|
||||
angular_damping: 0.0
|
||||
|
||||
ai:
|
||||
# Square: persecució contínua del ship més proper (steering suau, "tanc lent").
|
||||
movement:
|
||||
type: chase
|
||||
chase_strength: 0.5 # Força/segon de la LERP cap a la direcció ideal (1.0 = ~1s per realinear)
|
||||
|
||||
animation:
|
||||
pulse:
|
||||
trigger_prob_per_second: 0.01
|
||||
duration_min: 1.0
|
||||
duration_max: 3.0
|
||||
amplitude_min: 0.08
|
||||
amplitude_max: 0.20
|
||||
frequency_min: 1.5
|
||||
frequency_max: 3.0
|
||||
rotation_accel:
|
||||
trigger_prob_per_second: 0.02
|
||||
duration_min: 3.0
|
||||
duration_max: 8.0
|
||||
multiplier_min: 0.3
|
||||
multiplier_max: 4.0
|
||||
|
||||
wounded:
|
||||
duration: 1.0
|
||||
blink_hz: 10.0
|
||||
|
||||
spawn:
|
||||
invulnerability_duration: 3.0
|
||||
invulnerability_brightness_start: 0.3
|
||||
invulnerability_brightness_end: 0.7
|
||||
invulnerability_scale_start: 0.0
|
||||
invulnerability_scale_end: 1.0
|
||||
safety_distance: 36.0
|
||||
|
||||
colors:
|
||||
normal: [255, 0, 0] # Roig pur "tanc"
|
||||
wounded: [255, 220, 60]
|
||||
|
||||
score: 150
|
||||
|
||||
events:
|
||||
# HP=1 (default): decrement → on_no_health → set_hurt → wounded → mort.
|
||||
on_hit:
|
||||
- action: decrease_health
|
||||
- action: apply_impulse
|
||||
on_no_health:
|
||||
- action: set_hurt
|
||||
on_hurt_end:
|
||||
- action: destroy
|
||||
on_destroy:
|
||||
- action: add_score
|
||||
- action: create_debris
|
||||
- action: create_fireworks
|
||||
@@ -0,0 +1,77 @@
|
||||
name: star
|
||||
ai_type: star # Validat contra el directori; mapeja a EnemyType::STAR.
|
||||
|
||||
shape:
|
||||
path: enemy/star.shp
|
||||
scale: 0.7 # Lleugerament més petit que els altres enemics per diferenciar visualment.
|
||||
collision_factor: 1.0
|
||||
|
||||
physics:
|
||||
mass: 5.0
|
||||
speed: 35.0 # Mateixos paràmetres que pentagon (esquivador lent).
|
||||
rotation_delta_min: 0.75
|
||||
rotation_delta_max: 3.75
|
||||
restitution: 1.0
|
||||
linear_damping: 0.0
|
||||
angular_damping: 0.0
|
||||
|
||||
ai:
|
||||
# Movement: zigzag esquivador (com Pentagon).
|
||||
movement:
|
||||
type: zigzag
|
||||
angle_change_max: 1.0
|
||||
zigzag_prob_per_second: 0.8
|
||||
# Accions periòdiques: cada ~2.5s dispara una bala apuntada al ship més proper.
|
||||
tick:
|
||||
- action: shoot
|
||||
interval: 2.5
|
||||
aim_mode: aimed # apunta al ship més proper (atan2)
|
||||
jitter_rad: 0.0 # sense soroll: tret perfecte
|
||||
bullet: bullet_long # variant més visible per al jugador
|
||||
bullet_speed: 150.0 # px/s — prou lenta per reaccionar
|
||||
|
||||
animation:
|
||||
pulse:
|
||||
trigger_prob_per_second: 0.01
|
||||
duration_min: 1.0
|
||||
duration_max: 3.0
|
||||
amplitude_min: 0.08
|
||||
amplitude_max: 0.20
|
||||
frequency_min: 1.5
|
||||
frequency_max: 3.0
|
||||
rotation_accel:
|
||||
trigger_prob_per_second: 0.02
|
||||
duration_min: 3.0
|
||||
duration_max: 8.0
|
||||
multiplier_min: 0.3
|
||||
multiplier_max: 4.0
|
||||
|
||||
wounded:
|
||||
duration: 1.0
|
||||
blink_hz: 10.0
|
||||
|
||||
spawn:
|
||||
invulnerability_duration: 3.0
|
||||
invulnerability_brightness_start: 0.3
|
||||
invulnerability_brightness_end: 0.7
|
||||
invulnerability_scale_start: 0.0
|
||||
invulnerability_scale_end: 1.0
|
||||
safety_distance: 36.0
|
||||
|
||||
colors:
|
||||
normal: [255, 255, 0] # Groc estrella
|
||||
wounded: [255, 220, 60]
|
||||
|
||||
score: 100
|
||||
|
||||
events:
|
||||
# STAR: mor al primer impacte, sense passar per wounded.
|
||||
# HP=1 (default): decrement → on_no_health → destroy directe (sense wounded).
|
||||
on_hit:
|
||||
- action: decrease_health
|
||||
on_no_health:
|
||||
- action: destroy
|
||||
on_destroy:
|
||||
- action: add_score
|
||||
- action: create_debris
|
||||
- action: create_fireworks
|
||||
@@ -0,0 +1,116 @@
|
||||
# Orni Attack - locale: Catala (valencia)
|
||||
# Interficie traduida; pool in-game identic a en.yaml (es queda en angles).
|
||||
# Tots els textos en ASCII: VectorText no suporta caracters accentuats.
|
||||
|
||||
notification:
|
||||
press_again_exit: "PREMEU ESC UN ALTRE COP PER EIXIR"
|
||||
zoom: "ZOOM: {z}X"
|
||||
fullscreen_on: "PANTALLA COMPLETA"
|
||||
fullscreen_off: "MODE FINESTRA"
|
||||
vsync_on: "VSYNC ACTIU"
|
||||
vsync_off: "VSYNC INACTIU"
|
||||
antialias_on: "AA ACTIU"
|
||||
antialias_off: "AA INACTIU"
|
||||
postfx_on: "POSTPROCESSAT ACTIU"
|
||||
postfx_off: "POSTPROCESSAT INACTIU"
|
||||
screenshot: "IMATGE {file} GUARDADA A {folder}"
|
||||
locale_switched: "IDIOMA: {lang}"
|
||||
gamepad_connected: "{name} CONNECTAT"
|
||||
gamepad_disconnected: "{name} DESCONNECTAT"
|
||||
|
||||
language:
|
||||
ca: "CATALA"
|
||||
en: "ANGLES"
|
||||
|
||||
hud:
|
||||
level: "NIVELL "
|
||||
|
||||
title:
|
||||
press_start: "PREMEU START PER JUGAR"
|
||||
|
||||
demo:
|
||||
banner: "MODE DEMO - PREMEU START"
|
||||
|
||||
game_screen:
|
||||
game_over: "FI DEL JOC"
|
||||
continue: "CONTINUAR"
|
||||
continues_left: "CONTINUACIONS: {n}"
|
||||
|
||||
stage:
|
||||
start:
|
||||
- "ORNI ALERT!"
|
||||
- "INCOMING ORNIS!"
|
||||
- "ROLLING THREAT!"
|
||||
- "ENEMY WAVE!"
|
||||
- "WAVE OF ORNIS DETECTED!"
|
||||
- "NEXT SWARM APPROACHING!"
|
||||
- "BRACE FOR THE NEXT WAVE!"
|
||||
- "ANOTHER ATTACK INCOMING!"
|
||||
- "SENSORS DETECT HOSTILE ORNIS..."
|
||||
- "UNIDENTIFIED ROLLING OBJECTS INBOUND!"
|
||||
- "ENEMY FORCES MOBILIZING!"
|
||||
- "PREPARE FOR IMPACT!"
|
||||
completed: "GOOD JOB COMMANDER!"
|
||||
|
||||
service_menu:
|
||||
title: "MENU DE SERVEI"
|
||||
video: "VIDEO"
|
||||
audio: "AUDIO"
|
||||
options: "OPCIONS"
|
||||
system: "SISTEMA"
|
||||
controls: "CONTROLS"
|
||||
back: "ENRERE"
|
||||
exit: "EIXIR DEL JOC"
|
||||
# Items del submenu VIDEO
|
||||
video_zoom: "ZOOM"
|
||||
video_fullscreen: "PANTALLA COMPLETA"
|
||||
video_vsync: "VSYNC"
|
||||
video_aa: "ANTIALIAS"
|
||||
video_postfx: "POSTPROCESSAT"
|
||||
video_resolution: "RESOLUCIO"
|
||||
# Items del submenu OPCIONS
|
||||
options_language: "IDIOMA"
|
||||
options_show_info: "MOSTRAR INFO"
|
||||
# Items del submenu AUDIO
|
||||
audio_master: "AUDIO"
|
||||
audio_master_volume: "VOLUM GENERAL"
|
||||
audio_music: "MUSICA"
|
||||
audio_music_volume: "VOLUM MUSICA"
|
||||
audio_sound: "EFECTES"
|
||||
audio_sound_volume: "VOLUM EFECTES"
|
||||
# Items del submenu SISTEMA
|
||||
system_restart: "REINICIAR"
|
||||
# Pagines de confirmacio (estructura: titol + NO/SI)
|
||||
confirm_restart: "ESTAS SEGUR DE REINICIAR?"
|
||||
confirm_exit: "ESTAS SEGUR DE EIXIR?"
|
||||
confirm_no: "NO"
|
||||
confirm_yes: "SI"
|
||||
# Valors comuns
|
||||
value_on: "ACTIU"
|
||||
value_off: "INACTIU"
|
||||
# Items del submenu CONTROLS
|
||||
controls_pad_p1: "MANDO JUGADOR 1"
|
||||
controls_pad_p2: "MANDO JUGADOR 2"
|
||||
controls_no_pad: "SENSE MANDO"
|
||||
controls_define_keyboard_p1: "REDEFINIR TECLES P1"
|
||||
controls_define_keyboard_p2: "REDEFINIR TECLES P2"
|
||||
controls_define_gamepad_p1: "REDEFINIR BOTONS P1"
|
||||
controls_define_gamepad_p2: "REDEFINIR BOTONS P2"
|
||||
|
||||
# Overlay modal de redefinicio (DefineInputs)
|
||||
define:
|
||||
title_keyboard_p1: "REDEFINIR TECLES P1"
|
||||
title_keyboard_p2: "REDEFINIR TECLES P2"
|
||||
title_gamepad_p1: "REDEFINIR BOTONS P1"
|
||||
title_gamepad_p2: "REDEFINIR BOTONS P2"
|
||||
press_key: "PREMEU UNA TECLA"
|
||||
press_button: "PREMEU UN BOTO"
|
||||
complete: "CONFIGURACIO COMPLETA"
|
||||
no_gamepad: "CAP MANDO ASSIGNAT AL JUGADOR"
|
||||
action:
|
||||
left: "ESQUERRA"
|
||||
right: "DRETA"
|
||||
fire: "DISPARAR"
|
||||
accelerate: "ACCELERAR"
|
||||
start: "START"
|
||||
menu: "MENU"
|
||||
@@ -0,0 +1,115 @@
|
||||
# Orni Attack - locale: English
|
||||
# In-game pool kept English in both locales per design.
|
||||
|
||||
notification:
|
||||
press_again_exit: "PRESS ESC AGAIN TO EXIT"
|
||||
zoom: "ZOOM: {z}X"
|
||||
fullscreen_on: "FULLSCREEN"
|
||||
fullscreen_off: "WINDOWED"
|
||||
vsync_on: "VSYNC ON"
|
||||
vsync_off: "VSYNC OFF"
|
||||
antialias_on: "AA ON"
|
||||
antialias_off: "AA OFF"
|
||||
postfx_on: "POSTPROCESS ON"
|
||||
postfx_off: "POSTPROCESS OFF"
|
||||
screenshot: "IMAGE {file} SAVED AT {folder}"
|
||||
locale_switched: "LANGUAGE: {lang}"
|
||||
gamepad_connected: "{name} CONNECTED"
|
||||
gamepad_disconnected: "{name} DISCONNECTED"
|
||||
|
||||
language:
|
||||
ca: "CATALAN"
|
||||
en: "ENGLISH"
|
||||
|
||||
hud:
|
||||
level: "LEVEL "
|
||||
|
||||
title:
|
||||
press_start: "PRESS START TO PLAY"
|
||||
|
||||
demo:
|
||||
banner: "DEMO MODE - PRESS START"
|
||||
|
||||
game_screen:
|
||||
game_over: "GAME OVER"
|
||||
continue: "CONTINUE"
|
||||
continues_left: "CONTINUES LEFT: {n}"
|
||||
|
||||
stage:
|
||||
start:
|
||||
- "ORNI ALERT!"
|
||||
- "INCOMING ORNIS!"
|
||||
- "ROLLING THREAT!"
|
||||
- "ENEMY WAVE!"
|
||||
- "WAVE OF ORNIS DETECTED!"
|
||||
- "NEXT SWARM APPROACHING!"
|
||||
- "BRACE FOR THE NEXT WAVE!"
|
||||
- "ANOTHER ATTACK INCOMING!"
|
||||
- "SENSORS DETECT HOSTILE ORNIS..."
|
||||
- "UNIDENTIFIED ROLLING OBJECTS INBOUND!"
|
||||
- "ENEMY FORCES MOBILIZING!"
|
||||
- "PREPARE FOR IMPACT!"
|
||||
completed: "GOOD JOB COMMANDER!"
|
||||
|
||||
service_menu:
|
||||
title: "SERVICE MENU"
|
||||
video: "VIDEO"
|
||||
audio: "AUDIO"
|
||||
options: "OPTIONS"
|
||||
system: "SYSTEM"
|
||||
controls: "CONTROLS"
|
||||
back: "BACK"
|
||||
exit: "EXIT GAME"
|
||||
# Items of VIDEO submenu
|
||||
video_zoom: "ZOOM"
|
||||
video_fullscreen: "FULLSCREEN"
|
||||
video_vsync: "VSYNC"
|
||||
video_aa: "ANTIALIAS"
|
||||
video_postfx: "POSTPROCESS"
|
||||
video_resolution: "RESOLUTION"
|
||||
# Items of OPTIONS submenu
|
||||
options_language: "LANGUAGE"
|
||||
options_show_info: "SHOW INFO"
|
||||
# Items of AUDIO submenu
|
||||
audio_master: "AUDIO"
|
||||
audio_master_volume: "MASTER VOLUME"
|
||||
audio_music: "MUSIC"
|
||||
audio_music_volume: "MUSIC VOLUME"
|
||||
audio_sound: "SOUNDS"
|
||||
audio_sound_volume: "SOUND VOLUME"
|
||||
# Items of SYSTEM submenu
|
||||
system_restart: "RESTART"
|
||||
# Confirmation pages (structure: title + NO/YES)
|
||||
confirm_restart: "REALLY RESTART?"
|
||||
confirm_exit: "REALLY EXIT?"
|
||||
confirm_no: "NO"
|
||||
confirm_yes: "YES"
|
||||
# Common values
|
||||
value_on: "ON"
|
||||
value_off: "OFF"
|
||||
# Items of CONTROLS submenu
|
||||
controls_pad_p1: "PLAYER 1 GAMEPAD"
|
||||
controls_pad_p2: "PLAYER 2 GAMEPAD"
|
||||
controls_no_pad: "NO GAMEPAD"
|
||||
controls_define_keyboard_p1: "REDEFINE KEYS P1"
|
||||
controls_define_keyboard_p2: "REDEFINE KEYS P2"
|
||||
controls_define_gamepad_p1: "REDEFINE BUTTONS P1"
|
||||
controls_define_gamepad_p2: "REDEFINE BUTTONS P2"
|
||||
|
||||
# Modal overlay for input redefinition (DefineInputs)
|
||||
define:
|
||||
title_keyboard_p1: "REDEFINE KEYS P1"
|
||||
title_keyboard_p2: "REDEFINE KEYS P2"
|
||||
title_gamepad_p1: "REDEFINE BUTTONS P1"
|
||||
title_gamepad_p2: "REDEFINE BUTTONS P2"
|
||||
press_key: "PRESS A KEY"
|
||||
press_button: "PRESS A BUTTON"
|
||||
complete: "CONFIGURATION COMPLETE"
|
||||
no_gamepad: "NO GAMEPAD ASSIGNED TO PLAYER"
|
||||
action:
|
||||
left: "LEFT"
|
||||
right: "RIGHT"
|
||||
fire: "FIRE"
|
||||
accelerate: "ACCELERATE"
|
||||
start: "START"
|
||||
menu: "MENU"
|
||||
@@ -1,6 +1,6 @@
|
||||
# bullet.shp - Projectil (octàgon, radi=3)
|
||||
# bullet/basic.shp - Projectil (octàgon, radi=3)
|
||||
|
||||
name: bullet
|
||||
name: basic
|
||||
scale: 1.0
|
||||
center: 0, 0
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# bullet/double.shp - Bala anular (dos cercles concèntrics)
|
||||
# © 2026 JailDesigner
|
||||
#
|
||||
# Dos octàgons concèntrics al centre (0,0):
|
||||
# - Exterior: radi 4 (lleugerament més gran que la bala estàndard, radi 3)
|
||||
# - Interior: radi 2 (lleugerament més petit que la bala estàndard)
|
||||
# Aspecte d'anell / aura de plasma. Bounding radius natiu = 4.
|
||||
|
||||
name: double
|
||||
scale: 1.0
|
||||
center: 0, 0
|
||||
|
||||
# Cercle exterior (octàgon, radi 4)
|
||||
polyline: 0,-4 2.83,-2.83 4,0 2.83,2.83 0,4 -2.83,2.83 -4,0 -2.83,-2.83 0,-4
|
||||
|
||||
# Cercle interior (octàgon, radi 2)
|
||||
polyline: 0,-2 1.41,-1.41 2,0 1.41,1.41 0,2 -1.41,1.41 -2,0 -1.41,-1.41 0,-2
|
||||
@@ -0,0 +1,32 @@
|
||||
# bullet/long.shp - Bala allargada vertical (dos mig-octàgons + dos costats)
|
||||
# © 2026 JailDesigner
|
||||
#
|
||||
# Càpsula orientada al llarg de l'eix Y: la bala viatja segons el seu angle
|
||||
# de moviment (angle=0 = Y negatiu), i així s'estira en la direcció de vol.
|
||||
# Es dibuixen només els segments exteriors per evitar veure la unió interna
|
||||
# dels dos cercles; el resultat visual són dos "mig-octàgons" separats per
|
||||
# un petit gap al centre, units pels dos costats verticals.
|
||||
#
|
||||
# Geometria:
|
||||
# Mig-octàgon superior (radi 3) centrat a (0, -3)
|
||||
# Mig-octàgon inferior (radi 3) centrat a (0, 3)
|
||||
# Punt extrem superior: (0, -6)
|
||||
# Punt extrem inferior: (0, 6)
|
||||
# Bounding radius natiu = 6 (extrem vertical a y=±6).
|
||||
# collision_factor al YAML compensa el bounding doble (0.5 → hitbox ≈ 3).
|
||||
|
||||
name: long
|
||||
scale: 1.0
|
||||
center: 0, 0
|
||||
|
||||
# Mig-octàgon superior (5 vèrtexs: del cantó dret cap al punt extrem i a l'esquerre)
|
||||
polyline: 3,-3 2.12,-5.12 0,-6 -2.12,-5.12 -3,-3
|
||||
|
||||
# Mig-octàgon inferior
|
||||
polyline: 3,3 2.12,5.12 0,6 -2.12,5.12 -3,3
|
||||
|
||||
# Costat dret (uneix extrem inferior del mig superior amb extrem superior del mig inferior)
|
||||
polyline: 3,-3 3,3
|
||||
|
||||
# Costat esquerre
|
||||
polyline: -3,-3 -3,3
|
||||
@@ -1,7 +1,7 @@
|
||||
# star.shp - Estrella per a starfield
|
||||
# effect/starfield.shp - Estrella per a starfield
|
||||
# © 2026 JailDesigner
|
||||
|
||||
name: star
|
||||
name: starfield
|
||||
scale: 1.0
|
||||
center: 0, 0
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# effect/title_flash.shp - Sparkle 4-puntes amb costats còncaus (Atari-style)
|
||||
# 4 puntes als cardinals (radi 30) i valls còncaus als 45° (corba Bezier
|
||||
# quadràtica amb control point ±8). 5 punts per arc subdividint la corba.
|
||||
|
||||
name: title_flash
|
||||
scale: 1.0
|
||||
center: 0, 0
|
||||
|
||||
polyline: 0,-30 3.76,-21.76 8.64,-14.64 14.64,-8.64 21.76,-3.76 30,0 21.76,3.76 14.64,8.64 8.64,14.64 3.76,21.76 0,30 -3.76,21.76 -8.64,14.64 -14.64,8.64 -21.76,3.76 -30,0 -21.76,-3.76 -14.64,-8.64 -8.64,-14.64 -3.76,-21.76 0,-30
|
||||
@@ -0,0 +1,32 @@
|
||||
# enemy/orb.shp - ORNI enemic gegant (orb circular, doble anell amb radis)
|
||||
# © 2026 JailDesigner
|
||||
#
|
||||
# Forma "reactor / boss circular" — més detall que els enemics petits perquè
|
||||
# es renderitza a escala 1.5x i ha de llegir-se com a amenaça gran.
|
||||
# - Anell exterior: dodecàgon (12 vèrtexs) — aparença circular suau, radi 20.
|
||||
# - Anell interior: hexàgon (6 vèrtexs, rotat 30°) — radi 10.
|
||||
# - 6 radis curts que connecten l'anell interior amb l'exterior.
|
||||
# - Petit "+" central com a nucli.
|
||||
# Bounding radius natiu = 20 (alineat amb la resta d'enemics).
|
||||
|
||||
name: orb
|
||||
scale: 1.0
|
||||
center: 0, 0
|
||||
|
||||
# Anell exterior (dodecàgon, vèrtex apuntant amunt)
|
||||
polyline: 0,-20 10,-17.32 17.32,-10 20,0 17.32,10 10,17.32 0,20 -10,17.32 -17.32,10 -20,0 -17.32,-10 -10,-17.32 0,-20
|
||||
|
||||
# Anell interior (hexàgon, vèrtex apuntant a la dreta — rotat 30° respecte l'exterior)
|
||||
polyline: 5,-8.66 10,0 5,8.66 -5,8.66 -10,0 -5,-8.66 5,-8.66
|
||||
|
||||
# 6 radis: del vèrtex de l'hexàgon interior al vèrtex corresponent del dodecàgon exterior
|
||||
line: 5,-8.66 10,-17.32
|
||||
line: 10,0 20,0
|
||||
line: 5,8.66 10,17.32
|
||||
line: -5,8.66 -10,17.32
|
||||
line: -10,0 -20,0
|
||||
line: -5,-8.66 -10,-17.32
|
||||
|
||||
# Nucli central: petit "+" (2 segments creuats, radi 3)
|
||||
line: -3,0 3,0
|
||||
line: 0,-3 0,3
|
||||
@@ -0,0 +1,11 @@
|
||||
# enemy/pentagon.shp - ORNI enemic (pentàgon doble concentric, radi exterior=20)
|
||||
|
||||
name: pentagon
|
||||
scale: 1.0
|
||||
center: 0, 0
|
||||
|
||||
# Pentàgon exterior (vèrtex apuntant amunt, radi 20)
|
||||
polyline: 0,-20 19.02,-6.18 11.76,16.18 -11.76,16.18 -19.02,-6.18 0,-20
|
||||
|
||||
# Pentàgon interior (radi 10, rotat 36° → vèrtex apuntant a les arestes exteriors)
|
||||
polyline: 5.88,-8.09 9.51,3.09 0,10 -9.51,3.09 -5.88,-8.09 5.88,-8.09
|
||||
@@ -1,7 +1,7 @@
|
||||
# enemy_pinwheel.shp - ORNI enemic (molinillo de 4 triangles)
|
||||
# enemy/pinwheel.shp - ORNI enemic (molinillo de 4 triangles)
|
||||
# © 2026 JailDesigner
|
||||
|
||||
name: enemy_pinwheel
|
||||
name: pinwheel
|
||||
scale: 1.0
|
||||
center: 0, 0
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# enemy/square.shp - ORNI enemic (rombe, radi=20) + ull amb pupil·la al centre
|
||||
|
||||
name: square
|
||||
scale: 1.0
|
||||
center: 0, 0
|
||||
|
||||
# Rombe exterior
|
||||
polyline: 0,-20 20,0 0,20 -20,0 0,-20
|
||||
|
||||
# Ull (dos arcs units, forma d'almetlla). Amplada 20px, altura 8px.
|
||||
polyline: -10,0 -5,-3 0,-4 5,-3 10,0 5,3 0,4 -5,3 -10,0
|
||||
|
||||
# Pupil·la (octàgon, radi 2) al centre
|
||||
polyline: 0,-2 1.41,-1.41 2,0 1.41,1.41 0,2 -1.41,1.41 -2,0 -1.41,-1.41 0,-2
|
||||
@@ -0,0 +1,15 @@
|
||||
# enemy/star.shp - ORNI enemic (estrella de 5 puntes, només perímetre)
|
||||
# © 2026 JailDesigner
|
||||
#
|
||||
# Pentagrama clàssic: 5 vèrtexs exteriors (radi 20) alternant amb 5 vèrtexs
|
||||
# interiors (radi 7.64 = 20/φ² ≈ proporció àuria) per donar puntes esveltes.
|
||||
# Vèrtex apuntant amunt (igual que enemy_pentagon).
|
||||
#
|
||||
# Sense línies interiors: una única polyline que recorre el perímetre.
|
||||
# Bounding radius natiu ≈ 20 (alineat amb pentagon/square/pinwheel).
|
||||
|
||||
name: star
|
||||
scale: 1.0
|
||||
center: 0, 0
|
||||
|
||||
polyline: 0,-20 4.49,-6.18 19.02,-6.18 7.27,2.36 11.76,16.18 0,7.64 -11.76,16.18 -7.27,2.36 -19.02,-6.18 -4.49,-6.18 0,-20
|
||||
@@ -1,7 +0,0 @@
|
||||
# enemy_pentagon.shp - ORNI enemic (pentàgon regular, radi=20)
|
||||
|
||||
name: enemy_pentagon
|
||||
scale: 1.0
|
||||
center: 0, 0
|
||||
|
||||
polyline: 0,-20 19.02,-6.18 11.76,16.18 -11.76,16.18 -19.02,-6.18 0,-20
|
||||
@@ -1,7 +0,0 @@
|
||||
# enemy_square.shp - ORNI enemic (quadrat regular, radi=20)
|
||||
|
||||
name: enemy_square
|
||||
scale: 1.0
|
||||
center: 0, 0
|
||||
|
||||
polyline: 0,-20 20,0 0,20 -20,0 0,-20
|
||||
@@ -0,0 +1,9 @@
|
||||
# char_lparen.shp - Símbol ( (parèntesi esquerre)
|
||||
# Dimensions: 20×40 (blocky display)
|
||||
|
||||
name: char_lparen
|
||||
scale: 1.0
|
||||
center: 10, 20
|
||||
|
||||
# Arc cap a l'esquerra aproximat amb 4 trams rectes
|
||||
polyline: 14,4 8,12 6,20 8,28 14,36
|
||||
@@ -0,0 +1,9 @@
|
||||
# char_rparen.shp - Símbol ) (parèntesi dret)
|
||||
# Dimensions: 20×40 (blocky display)
|
||||
|
||||
name: char_rparen
|
||||
scale: 1.0
|
||||
center: 10, 20
|
||||
|
||||
# Arc cap a la dreta aproximat amb 4 trams rectes
|
||||
polyline: 6,4 12,12 14,20 12,28 6,36
|
||||
@@ -0,0 +1,9 @@
|
||||
# char_slash.shp - Símbol / (barra)
|
||||
# Dimensions: 20×40 (blocky display)
|
||||
|
||||
name: char_slash
|
||||
scale: 1.0
|
||||
center: 10, 20
|
||||
|
||||
# Línia diagonal de baix-esquerra a dalt-dreta
|
||||
line: 4,36 16,4
|
||||
@@ -0,0 +1,9 @@
|
||||
# char_underscore.shp - Símbolo _ (barra baja)
|
||||
# Dimensiones: 20×40 (blocky display)
|
||||
|
||||
name: char_underscore
|
||||
scale: 1.0
|
||||
center: 10, 20
|
||||
|
||||
# Línea horizontal abajo (bajo la baseline de las letras)
|
||||
line: 3,33 17,33
|
||||
@@ -1,8 +0,0 @@
|
||||
# ship.shp - Nau del jugador 1
|
||||
# Triangle amb base còncava (punta de fletxa)
|
||||
|
||||
name: ship
|
||||
scale: 1.0
|
||||
center: 0, 0
|
||||
|
||||
polyline: 0,-12 8.49,8.49 0,4 -8.49,8.49 0,-12
|
||||
@@ -0,0 +1,7 @@
|
||||
# ship/arrow.shp - Nau del jugador 1 (triangle amb base còncava, punta de fletxa)
|
||||
|
||||
name: arrow
|
||||
scale: 1.0
|
||||
center: 0, 0
|
||||
|
||||
polyline: 0,-12 8.49,8.49 0,4 -8.49,8.49 0,-12
|
||||
@@ -1,7 +1,7 @@
|
||||
# ship2.shp - Nau del jugador 2 (interceptor amb ales)
|
||||
# ship/interceptor.shp - Interceptor amb ales laterals pronunciades
|
||||
# © 2026 JailDesigner
|
||||
|
||||
name: ship2
|
||||
name: interceptor
|
||||
scale: 1.0
|
||||
center: 0, 0
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# ship2.shp - Nau del jugador 2
|
||||
# Triangle amb cercle central (distintiu visual)
|
||||
# ship/wedge.shp - Nau del jugador 2 (triangle amb cercle central)
|
||||
|
||||
name: ship2
|
||||
name: wedge
|
||||
scale: 1.0
|
||||
center: 0, 0
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
# ship2_perspective.shp - Nave P2 con perspectiva pre-calculada
|
||||
# Posición optimizada: "4 del reloj" (Abajo-Derecha)
|
||||
# Dirección: Volando hacia el fondo (centro pantalla)
|
||||
|
||||
name: ship2_perspective
|
||||
scale: 1.0
|
||||
center: 0, 0
|
||||
|
||||
# TRANSFORMACIÓN APLICADA:
|
||||
# 1. Rotación -45° (apuntando al centro desde abajo-dcha)
|
||||
# 2. Proyección de perspectiva:
|
||||
# - Punta (p1): Reducida al 60% (simula lejanía)
|
||||
# - Base (p2, p3): Aumentada al 110% (simula cercanía)
|
||||
# 3. Flip horizontal (simétrica a ship_starfield.shp)
|
||||
#
|
||||
# Nuevos Punts (aprox):
|
||||
# p1 (Punta): (-4, -4) -> Lejos, pequeña y apuntando arriba-izq
|
||||
# p2 (Ala Izq): (-3, 11) -> Cerca, lado interior
|
||||
# p4 (Base Cnt): (3, 5) -> Centro base
|
||||
# p3 (Ala Dcha): (11, 2) -> Cerca, lado exterior (más grande)
|
||||
|
||||
#polyline: -4,-4 -3,11 3,5 11,2 -4,-4
|
||||
polyline: -4,-4 -3,11 11,2 -4,-4
|
||||
|
||||
# Circulito central (octàgon r=2.5)
|
||||
# Distintiu visual del jugador 2
|
||||
# Sin perspectiva (está en el centro de la nave)
|
||||
polyline: 0,-2.5 1.77,-1.77 2.5,0 1.77,1.77 0,2.5 -1.77,1.77 -2.5,0 -1.77,-1.77 0,-2.5
|
||||
@@ -1,21 +0,0 @@
|
||||
# ship_perspective.shp - Nave con perspectiva pre-calculada
|
||||
# Posición optimizada: "8 del reloj" (Abajo-Izquierda)
|
||||
# Dirección: Volando hacia el fondo (centro pantalla)
|
||||
|
||||
name: ship_perspective
|
||||
scale: 1.0
|
||||
center: 0, 0
|
||||
|
||||
# TRANSFORMACIÓN APLICADA:
|
||||
# 1. Rotación +45° (apuntando al centro desde abajo-izq)
|
||||
# 2. Proyección de perspectiva:
|
||||
# - Punta (p1): Reducida al 60% (simula lejanía)
|
||||
# - Base (p2, p3): Aumentada al 110% (simula cercanía)
|
||||
#
|
||||
# Nuevos Puntos (aprox):
|
||||
# p1 (Punta): (4, -4) -> Lejos, pequeña y apuntando arriba-dcha
|
||||
# p2 (Ala Dcha): (3, 11) -> Cerca, lado interior
|
||||
# p4 (Base Cnt): (-3, 5) -> Centro base
|
||||
# p3 (Ala Izq): (-11, 2) -> Cerca, lado exterior (más grande)
|
||||
|
||||
polyline: 4,-4 3,11 -3,5 -11,2 4,-4
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+174
-159
@@ -1,168 +1,183 @@
|
||||
# stages.yaml - Configuració de les 10 etapes d'Orni Attack
|
||||
# stages.yaml - Configuració de les fases d'Orni Attack
|
||||
# © 2026 JailDesigner
|
||||
#
|
||||
# Format basat en onades (waves). Cada wave:
|
||||
# - spawn: list d'enemics a generar, en ordre.
|
||||
# - spawn_interval: segons entre spawns interns (default 0 = simultanis).
|
||||
# - next: condició per avançar a la wave següent.
|
||||
# - "all_dead" / "end" → quan tots els enemics de l'arena han mort.
|
||||
# - { timeout: T } → quan han passat T segons des de l'inici de la wave.
|
||||
# - { all_dead: true, timeout: T } → el que arribe abans (amuntegament si vas lent).
|
||||
#
|
||||
# Tipus d'enemic: pentagon, square (alias: cuadrado), pinwheel (alias: molinillo), star, orb.
|
||||
|
||||
metadata:
|
||||
version: "1.0"
|
||||
version: "2.0"
|
||||
total_stages: 10
|
||||
description: "Progressive difficulty curve from novice to expert"
|
||||
description: "Wave-based progression"
|
||||
|
||||
stages:
|
||||
# STAGE 1: Tutorial - Only pentagons, slow speed
|
||||
# STAGE 1 — Tutorial: contacte amb pentagons i un cuadrado.
|
||||
# (Test: també hi ha un orb a la primera onada per provar el contra-atac.)
|
||||
- stage_id: 1
|
||||
total_enemies: 50
|
||||
spawn_config:
|
||||
mode: "progressive"
|
||||
initial_delay: 0.3
|
||||
spawn_interval: 0.4
|
||||
enemy_distribution:
|
||||
pentagon: 100
|
||||
cuadrado: 0
|
||||
molinillo: 0
|
||||
difficulty_multipliers:
|
||||
speed_multiplier: 0.7
|
||||
rotation_multiplier: 0.8
|
||||
tracking_strength: 0.0
|
||||
|
||||
# STAGE 2: Introduction to tracking enemies
|
||||
- stage_id: 2
|
||||
total_enemies: 7
|
||||
spawn_config:
|
||||
mode: "progressive"
|
||||
initial_delay: 1.5
|
||||
spawn_interval: 2.5
|
||||
enemy_distribution:
|
||||
pentagon: 70
|
||||
cuadrado: 30
|
||||
molinillo: 0
|
||||
difficulty_multipliers:
|
||||
speed_multiplier: 0.85
|
||||
rotation_multiplier: 0.9
|
||||
tracking_strength: 0.3
|
||||
|
||||
# STAGE 3: All enemy types, normal speed
|
||||
- stage_id: 3
|
||||
total_enemies: 10
|
||||
spawn_config:
|
||||
mode: "progressive"
|
||||
initial_delay: 1.0
|
||||
spawn_interval: 2.0
|
||||
enemy_distribution:
|
||||
pentagon: 50
|
||||
cuadrado: 30
|
||||
molinillo: 20
|
||||
difficulty_multipliers:
|
||||
speed_multiplier: 1.0
|
||||
rotation_multiplier: 1.0
|
||||
tracking_strength: 0.5
|
||||
|
||||
# STAGE 4: Increased count, faster enemies
|
||||
- stage_id: 4
|
||||
total_enemies: 12
|
||||
spawn_config:
|
||||
mode: "progressive"
|
||||
initial_delay: 0.8
|
||||
spawn_interval: 1.8
|
||||
enemy_distribution:
|
||||
pentagon: 40
|
||||
cuadrado: 35
|
||||
molinillo: 25
|
||||
difficulty_multipliers:
|
||||
speed_multiplier: 1.1
|
||||
rotation_multiplier: 1.15
|
||||
tracking_strength: 0.6
|
||||
|
||||
# STAGE 5: Maximum count reached
|
||||
- stage_id: 5
|
||||
total_enemies: 15
|
||||
spawn_config:
|
||||
mode: "progressive"
|
||||
initial_delay: 0.5
|
||||
spawn_interval: 1.5
|
||||
enemy_distribution:
|
||||
pentagon: 35
|
||||
cuadrado: 35
|
||||
molinillo: 30
|
||||
difficulty_multipliers:
|
||||
speed_multiplier: 1.2
|
||||
rotation_multiplier: 1.25
|
||||
tracking_strength: 0.7
|
||||
|
||||
# STAGE 6: Molinillo becomes dominant
|
||||
- stage_id: 6
|
||||
total_enemies: 15
|
||||
spawn_config:
|
||||
mode: "progressive"
|
||||
initial_delay: 0.3
|
||||
spawn_interval: 1.3
|
||||
enemy_distribution:
|
||||
pentagon: 30
|
||||
cuadrado: 30
|
||||
molinillo: 40
|
||||
difficulty_multipliers:
|
||||
speed_multiplier: 1.3
|
||||
rotation_multiplier: 1.4
|
||||
tracking_strength: 0.8
|
||||
|
||||
# STAGE 7: High intensity, fast spawns
|
||||
- stage_id: 7
|
||||
total_enemies: 15
|
||||
spawn_config:
|
||||
mode: "progressive"
|
||||
initial_delay: 0.2
|
||||
spawn_interval: 1.0
|
||||
enemy_distribution:
|
||||
pentagon: 25
|
||||
cuadrado: 30
|
||||
molinillo: 45
|
||||
difficulty_multipliers:
|
||||
speed_multiplier: 1.4
|
||||
rotation_multiplier: 1.5
|
||||
tracking_strength: 0.9
|
||||
|
||||
# STAGE 8: Expert level, 50% molinillos
|
||||
- stage_id: 8
|
||||
total_enemies: 15
|
||||
spawn_config:
|
||||
mode: "progressive"
|
||||
initial_delay: 0.1
|
||||
spawn_interval: 0.8
|
||||
enemy_distribution:
|
||||
pentagon: 20
|
||||
cuadrado: 30
|
||||
molinillo: 50
|
||||
difficulty_multipliers:
|
||||
speed_multiplier: 1.5
|
||||
rotation_multiplier: 1.6
|
||||
tracking_strength: 1.0
|
||||
|
||||
# STAGE 9: Near-maximum difficulty
|
||||
- stage_id: 9
|
||||
total_enemies: 15
|
||||
spawn_config:
|
||||
mode: "progressive"
|
||||
initial_delay: 0.0
|
||||
multipliers: { velocity: 0.85, rotation: 0.9, tracking: 0.3 }
|
||||
waves:
|
||||
- spawn: [pentagon, pentagon, orb]
|
||||
spawn_interval: 0.6
|
||||
enemy_distribution:
|
||||
pentagon: 15
|
||||
cuadrado: 25
|
||||
molinillo: 60
|
||||
difficulty_multipliers:
|
||||
speed_multiplier: 1.6
|
||||
rotation_multiplier: 1.7
|
||||
tracking_strength: 1.1
|
||||
|
||||
# STAGE 10: Final challenge, 70% molinillos
|
||||
- stage_id: 10
|
||||
total_enemies: 15
|
||||
spawn_config:
|
||||
mode: "progressive"
|
||||
initial_delay: 0.0
|
||||
next: all_dead
|
||||
- spawn: [pentagon, pentagon, square]
|
||||
spawn_interval: 0.5
|
||||
enemy_distribution:
|
||||
pentagon: 10
|
||||
cuadrado: 20
|
||||
molinillo: 70
|
||||
difficulty_multipliers:
|
||||
speed_multiplier: 1.8
|
||||
rotation_multiplier: 2.0
|
||||
tracking_strength: 1.2
|
||||
next: all_dead
|
||||
- spawn: [pentagon, pentagon, square, square]
|
||||
spawn_interval: 0.4
|
||||
next: end
|
||||
|
||||
# STAGE 2 — Apareixen molinillos.
|
||||
- stage_id: 2
|
||||
multipliers: { velocity: 0.95, rotation: 1.0, tracking: 0.4 }
|
||||
waves:
|
||||
- spawn: [pentagon, pentagon, pentagon]
|
||||
spawn_interval: 0.5
|
||||
next: all_dead
|
||||
- spawn: [pinwheel]
|
||||
next: all_dead
|
||||
- spawn: [pentagon, square, pinwheel]
|
||||
spawn_interval: 0.6
|
||||
next: all_dead
|
||||
- spawn: [pinwheel, pinwheel, pentagon]
|
||||
spawn_interval: 0.5
|
||||
next: end
|
||||
|
||||
# STAGE 3 — Primer orb (HP=10).
|
||||
- stage_id: 3
|
||||
multipliers: { velocity: 1.0, rotation: 1.0, tracking: 0.5 }
|
||||
waves:
|
||||
- spawn: [pentagon, pentagon, square]
|
||||
spawn_interval: 0.4
|
||||
next: all_dead
|
||||
- spawn: [orb]
|
||||
next: { all_dead: true, timeout: 12.0 }
|
||||
- spawn: [pinwheel, pinwheel]
|
||||
spawn_interval: 0.5
|
||||
next: all_dead
|
||||
- spawn: [pentagon, square, pinwheel, pinwheel]
|
||||
spawn_interval: 0.4
|
||||
next: end
|
||||
|
||||
# STAGE 4 — Pressió creixent: timeouts curts que poden encavalcar onades.
|
||||
- stage_id: 4
|
||||
multipliers: { velocity: 1.05, rotation: 1.1, tracking: 0.6 }
|
||||
waves:
|
||||
- spawn: [pentagon, pentagon, pentagon]
|
||||
spawn_interval: 0.3
|
||||
next: { all_dead: true, timeout: 5.0 }
|
||||
- spawn: [square, square]
|
||||
spawn_interval: 0.4
|
||||
next: { all_dead: true, timeout: 6.0 }
|
||||
- spawn: [pinwheel, pinwheel, pinwheel]
|
||||
spawn_interval: 0.4
|
||||
next: all_dead
|
||||
- spawn: [orb, pentagon, pentagon]
|
||||
spawn_interval: 0.5
|
||||
next: end
|
||||
|
||||
# STAGE 5 — Apareix la star (zigzag clon del pentagon).
|
||||
- stage_id: 5
|
||||
multipliers: { velocity: 1.1, rotation: 1.2, tracking: 0.7 }
|
||||
waves:
|
||||
- spawn: [star, star]
|
||||
spawn_interval: 0.4
|
||||
next: all_dead
|
||||
- spawn: [pentagon, square, star]
|
||||
spawn_interval: 0.4
|
||||
next: { all_dead: true, timeout: 6.0 }
|
||||
- spawn: [pinwheel, pinwheel, star, star]
|
||||
spawn_interval: 0.4
|
||||
next: all_dead
|
||||
- spawn: [orb, square, square]
|
||||
spawn_interval: 0.5
|
||||
next: end
|
||||
|
||||
# STAGE 6 — Densitat alta, mix amb timeouts agressius.
|
||||
- stage_id: 6
|
||||
multipliers: { velocity: 1.15, rotation: 1.25, tracking: 0.8 }
|
||||
waves:
|
||||
- spawn: [pentagon, pinwheel, pentagon, pinwheel]
|
||||
spawn_interval: 0.3
|
||||
next: { all_dead: true, timeout: 5.0 }
|
||||
- spawn: [square, square, star]
|
||||
spawn_interval: 0.4
|
||||
next: { all_dead: true, timeout: 5.0 }
|
||||
- spawn: [pinwheel, pinwheel, pinwheel]
|
||||
spawn_interval: 0.3
|
||||
next: all_dead
|
||||
- spawn: [orb, pinwheel, pinwheel]
|
||||
spawn_interval: 0.4
|
||||
next: end
|
||||
|
||||
# STAGE 7 — Tiradors i agressivitat.
|
||||
- stage_id: 7
|
||||
multipliers: { velocity: 1.25, rotation: 1.35, tracking: 0.9 }
|
||||
waves:
|
||||
- spawn: [square, square, square]
|
||||
spawn_interval: 0.5
|
||||
next: { all_dead: true, timeout: 6.0 }
|
||||
- spawn: [pinwheel, pinwheel, pentagon, pentagon]
|
||||
spawn_interval: 0.3
|
||||
next: { all_dead: true, timeout: 5.0 }
|
||||
- spawn: [star, star, star]
|
||||
spawn_interval: 0.4
|
||||
next: all_dead
|
||||
- spawn: [orb, pinwheel, pinwheel, square]
|
||||
spawn_interval: 0.5
|
||||
next: end
|
||||
|
||||
# STAGE 8 — Pressió constant.
|
||||
- stage_id: 8
|
||||
multipliers: { velocity: 1.35, rotation: 1.45, tracking: 1.0 }
|
||||
waves:
|
||||
- spawn: [pinwheel, pinwheel, pinwheel]
|
||||
spawn_interval: 0.3
|
||||
next: { all_dead: true, timeout: 4.0 }
|
||||
- spawn: [square, square, star, star]
|
||||
spawn_interval: 0.3
|
||||
next: { all_dead: true, timeout: 5.0 }
|
||||
- spawn: [orb]
|
||||
next: { all_dead: true, timeout: 8.0 }
|
||||
- spawn: [pinwheel, pinwheel, square, star, pentagon]
|
||||
spawn_interval: 0.3
|
||||
next: end
|
||||
|
||||
# STAGE 9 — Quasi-final.
|
||||
- stage_id: 9
|
||||
multipliers: { velocity: 1.5, rotation: 1.6, tracking: 1.1 }
|
||||
waves:
|
||||
- spawn: [pinwheel, pinwheel, star, star]
|
||||
spawn_interval: 0.3
|
||||
next: { all_dead: true, timeout: 4.0 }
|
||||
- spawn: [orb, square, square]
|
||||
spawn_interval: 0.4
|
||||
next: { all_dead: true, timeout: 8.0 }
|
||||
- spawn: [pinwheel, pinwheel, pinwheel, pinwheel]
|
||||
spawn_interval: 0.3
|
||||
next: { all_dead: true, timeout: 5.0 }
|
||||
- spawn: [orb, pinwheel, pinwheel, square, star]
|
||||
spawn_interval: 0.4
|
||||
next: end
|
||||
|
||||
# STAGE 10 — Repte final.
|
||||
- stage_id: 10
|
||||
multipliers: { velocity: 1.7, rotation: 1.8, tracking: 1.2 }
|
||||
waves:
|
||||
- spawn: [pinwheel, pinwheel, pinwheel, pinwheel]
|
||||
spawn_interval: 0.25
|
||||
next: { all_dead: true, timeout: 4.0 }
|
||||
- spawn: [orb, square, star]
|
||||
spawn_interval: 0.4
|
||||
next: { all_dead: true, timeout: 6.0 }
|
||||
- spawn: [pinwheel, pinwheel, star, star, square]
|
||||
spawn_interval: 0.3
|
||||
next: { all_dead: true, timeout: 5.0 }
|
||||
- spawn: [orb, orb, pinwheel, pinwheel, star]
|
||||
spawn_interval: 0.4
|
||||
next: end
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 361 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 174 KiB After Width: | Height: | Size: 537 KiB |
@@ -29,7 +29,7 @@
|
||||
<key>CSResourcesFileMapped</key>
|
||||
<true/>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.15</string>
|
||||
<string>13.3</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<true/>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
|
||||
+43
-29
@@ -51,8 +51,6 @@ void Audio::playMusic(const std::string& name, const int loop, const int crossfa
|
||||
return;
|
||||
}
|
||||
|
||||
if (!music_enabled_) { return; }
|
||||
|
||||
auto* resource = AudioResource::getMusic(name);
|
||||
if (resource == nullptr) { return; }
|
||||
|
||||
@@ -62,7 +60,7 @@ void Audio::playMusic(const std::string& name, const int loop, const int crossfa
|
||||
|
||||
// Reprodueix la música per punter (amb crossfade opcional)
|
||||
void Audio::playMusic(Ja::Music* music, const int loop, const int crossfade_ms) {
|
||||
if (!music_enabled_ || music == nullptr) { return; }
|
||||
if (music == nullptr) { return; }
|
||||
|
||||
playMusicInternal(music, loop, crossfade_ms);
|
||||
// Si el Ja::Music es va crear con filename (loadMusic con 3 arguments), el
|
||||
@@ -72,9 +70,12 @@ void Audio::playMusic(Ja::Music* music, const int loop, const int crossfade_ms)
|
||||
}
|
||||
|
||||
// Camí comú dels dos overloads: fa el dispatch crossfade vs stop+play i
|
||||
// actualitza el loop cachejat. Els callers s'encarreguen del gating
|
||||
// (music_enabled_, nullptr, same-track early return) y del nom. L'estat el
|
||||
// manté Ja (Ja::playMusic posa PLAYING al Ja::Music* corresponent).
|
||||
// actualitza el loop cachejat. Els callers s'encarreguen del same-track early
|
||||
// return i del nom. El gate de música deshabilitada NO atura la reproducció:
|
||||
// effectiveVolume porta el volum efectiu a 0 i la pista continua sonant
|
||||
// silenciada, per garantir que reactivar la música la torne a sentir sense
|
||||
// haver de reiniciar la pista. L'estat el manté Ja (Ja::playMusic posa
|
||||
// PLAYING al Ja::Music* corresponent).
|
||||
void Audio::playMusicInternal(Ja::Music* music, const int loop, const int crossfade_ms) {
|
||||
const bool CURRENTLY_PLAYING = (getMusicState() == MusicState::PLAYING);
|
||||
if (crossfade_ms > 0 && CURRENTLY_PLAYING) {
|
||||
@@ -91,41 +92,35 @@ void Audio::playMusicInternal(Ja::Music* music, const int loop, const int crossf
|
||||
|
||||
// Pausa la música (l'estat el transiciona Engine::pauseMusic)
|
||||
void Audio::pauseMusic() {
|
||||
if (music_enabled_ && getMusicState() == MusicState::PLAYING) {
|
||||
if (getMusicState() == MusicState::PLAYING) {
|
||||
engine_->pauseMusic();
|
||||
}
|
||||
}
|
||||
|
||||
// Continua la música pausada (l'estat el transiciona Engine::resumeMusic)
|
||||
void Audio::resumeMusic() {
|
||||
if (music_enabled_ && getMusicState() == MusicState::PAUSED) {
|
||||
if (getMusicState() == MusicState::PAUSED) {
|
||||
engine_->resumeMusic();
|
||||
}
|
||||
}
|
||||
|
||||
// Atura la música (l'estat el transiciona Engine::stopMusic)
|
||||
void Audio::stopMusic() {
|
||||
if (music_enabled_) {
|
||||
engine_->stopMusic();
|
||||
}
|
||||
}
|
||||
|
||||
void Audio::setMusicSpeed(float ratio) {
|
||||
if (music_enabled_) {
|
||||
engine_->setMusicSpeed(ratio);
|
||||
}
|
||||
}
|
||||
|
||||
// Reprodueix un so per nom
|
||||
void Audio::playSound(const std::string& name, Group group) {
|
||||
if (sound_enabled_) {
|
||||
engine_->playSound(AudioResource::getSound(name), 0, static_cast<int>(group));
|
||||
}
|
||||
}
|
||||
|
||||
// Reprodueix un so per punter directe
|
||||
void Audio::playSound(Ja::Sound* sound, Group group) {
|
||||
if (sound_enabled_ && sound != nullptr) {
|
||||
if (sound != nullptr) {
|
||||
engine_->playSound(sound, 0, static_cast<int>(group));
|
||||
}
|
||||
}
|
||||
@@ -136,7 +131,6 @@ void Audio::playSound(Ja::Sound* sound, Group group) {
|
||||
// Si l'engine torna -1 (sense canal lliure) o el so no existeix, no fem
|
||||
// la crida al ratio — sin efectes col·laterals.
|
||||
void Audio::playSound(const std::string& name, Group group, float speed) {
|
||||
if (!sound_enabled_) { return; }
|
||||
auto* sound = AudioResource::getSound(name);
|
||||
if (sound == nullptr) { return; }
|
||||
const int CH = engine_->playSound(sound, 0, static_cast<int>(group));
|
||||
@@ -149,7 +143,6 @@ void Audio::playSound(const std::string& name, Group group, float speed) {
|
||||
// existeix o l'engine retorna -1 (sin de canals d'efecte plé), cau a playSound
|
||||
// sec — l'usuari sent el so aún que la cua no s'apliqui.
|
||||
void Audio::playSoundWithEcho(const std::string& name, const std::string& preset_name, Group group) {
|
||||
if (!sound_enabled_) { return; }
|
||||
auto* sound = AudioResource::getSound(name);
|
||||
if (sound == nullptr) { return; }
|
||||
|
||||
@@ -168,7 +161,6 @@ void Audio::playSoundWithEcho(const std::string& name, const std::string& preset
|
||||
// Reprodueix un so processat per un reverb definit a sounds.yaml. Mateix
|
||||
// fallback que playSoundWithEcho.
|
||||
void Audio::playSoundWithReverb(const std::string& name, const std::string& preset_name, Group group) {
|
||||
if (!sound_enabled_) { return; }
|
||||
auto* sound = AudioResource::getSound(name);
|
||||
if (sound == nullptr) { return; }
|
||||
|
||||
@@ -186,14 +178,12 @@ void Audio::playSoundWithReverb(const std::string& name, const std::string& pres
|
||||
|
||||
// Atura tots los sons
|
||||
void Audio::stopAllSounds() {
|
||||
if (sound_enabled_) {
|
||||
engine_->stopChannel(-1);
|
||||
}
|
||||
}
|
||||
|
||||
// Fa una fosa de sortida de la música
|
||||
void Audio::fadeOutMusic(int milliseconds) {
|
||||
if (music_enabled_ && getMusicState() == MusicState::PLAYING) {
|
||||
if (getMusicState() == MusicState::PLAYING) {
|
||||
engine_->fadeOutMusic(milliseconds);
|
||||
}
|
||||
}
|
||||
@@ -238,14 +228,27 @@ auto Audio::effectiveVolume(float volume, bool channel_enabled) const -> float {
|
||||
return (enabled_ && channel_enabled) ? volume * config_.volume : 0.0F;
|
||||
}
|
||||
|
||||
// Estableix el volum dels sons (float 0.0..1.0)
|
||||
// Estableix el volum dels sons (float 0.0..1.0). Actualitza el valor cachejat
|
||||
// a config_ perquè els getters i les re-aplicacions internes (enableSound,
|
||||
// setMasterVolume) puguin tornar al volum que l'usuari va triar.
|
||||
void Audio::setSoundVolume(float sound_volume, Group group) {
|
||||
engine_->setSoundVolume(effectiveVolume(sound_volume, sound_enabled_), static_cast<int>(group));
|
||||
config_.sound_volume = std::clamp(sound_volume, MIN_VOLUME, MAX_VOLUME);
|
||||
engine_->setSoundVolume(effectiveVolume(config_.sound_volume, sound_enabled_), static_cast<int>(group));
|
||||
}
|
||||
|
||||
// Estableix el volum de la música (float 0.0..1.0)
|
||||
// Estableix el volum de la música (float 0.0..1.0). Cf. setSoundVolume.
|
||||
void Audio::setMusicVolume(float music_volume) {
|
||||
engine_->setMusicVolume(effectiveVolume(music_volume, music_enabled_));
|
||||
config_.music_volume = std::clamp(music_volume, MIN_VOLUME, MAX_VOLUME);
|
||||
engine_->setMusicVolume(effectiveVolume(config_.music_volume, music_enabled_));
|
||||
}
|
||||
|
||||
// Estableix el volum master (multiplicador aplicat a sound + music). Re-aplica
|
||||
// els canals perquè el canvi tingui efecte immediat sense esperar al següent
|
||||
// setSoundVolume/setMusicVolume explícit.
|
||||
void Audio::setMasterVolume(float master_volume) {
|
||||
config_.volume = std::clamp(master_volume, MIN_VOLUME, MAX_VOLUME);
|
||||
setSoundVolume(config_.sound_volume);
|
||||
setMusicVolume(config_.music_volume);
|
||||
}
|
||||
|
||||
// Aplica una nueva configuración (substitueix la config cachejada i reaplica enables/volums)
|
||||
@@ -256,12 +259,12 @@ void Audio::applySettings(const Config& config) {
|
||||
enable(config_.enabled);
|
||||
}
|
||||
|
||||
// Estableix l'estat general
|
||||
// Estableix l'estat general. Re-aplica els volums actuals; effectiveVolume
|
||||
// retalla a 0 quan enabled_ és false, sense perdre els valors guardats.
|
||||
void Audio::enable(bool value) {
|
||||
enabled_ = value;
|
||||
|
||||
setSoundVolume(enabled_ ? config_.sound_volume : MIN_VOLUME);
|
||||
setMusicVolume(enabled_ ? config_.music_volume : MIN_VOLUME);
|
||||
setSoundVolume(config_.sound_volume);
|
||||
setMusicVolume(config_.music_volume);
|
||||
}
|
||||
|
||||
// Estableix l'estat dels sons i reaplica el volum porque los canals actius
|
||||
@@ -278,6 +281,17 @@ void Audio::enableMusic(bool value) {
|
||||
setMusicVolume(config_.music_volume);
|
||||
}
|
||||
|
||||
// Silencia o restaura un grup de sons concret sense alterar config_ (el volum
|
||||
// que l'usuari va triar) ni els altres grups. Silenciar posa la ganancia del
|
||||
// grup a 0; restaurar-la torna al volum efectiu normal (que ja aplica els gates
|
||||
// master/sound i el volum de l'usuari). A diferència de setSoundVolume, no
|
||||
// xafa config_.sound_volume, así que el menu de servei segueix mostrant i
|
||||
// operant el volum real durant la demo.
|
||||
void Audio::silenceGroup(Group group, bool silenced) {
|
||||
const float VOL = silenced ? 0.0F : effectiveVolume(config_.sound_volume, sound_enabled_);
|
||||
engine_->setSoundVolume(VOL, static_cast<int>(group));
|
||||
}
|
||||
|
||||
// Inicialitza SDL Audio y el motor Ja::Engine owned.
|
||||
void Audio::initSDLAudio() {
|
||||
if (!SDL_Init(SDL_INIT_AUDIO)) {
|
||||
|
||||
@@ -101,6 +101,14 @@ class Audio {
|
||||
// --- Control de volum (API interna: float 0.0..1.0) ---
|
||||
void setSoundVolume(float volume, Group group = Group::ALL); // Ajusta el volum dels efectes
|
||||
void setMusicVolume(float volume); // Ajusta el volum de la música
|
||||
void setMasterVolume(float volume); // Ajusta el master (re-aplica sound + music)
|
||||
|
||||
// Getters dels volums actuals (lectura de la config_ cachejada). Reflexen
|
||||
// el valor que l'usuari ha triat l'última vegada, independent del gating
|
||||
// d'enabled/channel.
|
||||
[[nodiscard]] auto getMasterVolume() const -> float { return config_.volume; }
|
||||
[[nodiscard]] auto getSoundVolume() const -> float { return config_.sound_volume; }
|
||||
[[nodiscard]] auto getMusicVolume() const -> float { return config_.music_volume; }
|
||||
|
||||
// --- Helpers de conversió para la capa de presentació ---
|
||||
// UI (menús, notificacions) manega enters 0..100; internament viu float 0..1.
|
||||
@@ -120,6 +128,12 @@ class Audio {
|
||||
// --- Configuración de sons ---
|
||||
void enableSound(bool value); // Estableix l'estat dels sons (reaplica volum)
|
||||
void toggleSound() { enableSound(!sound_enabled_); } // Alterna l'estat dels sons (reaplica volum)
|
||||
// Silencia (o restaura) un únic grup de sons sense tocar el volum cachejat
|
||||
// de l'usuari ni la resta de grups. Pensat per a l'attract/demo: vol callar
|
||||
// els SFX de joc (Group::GAME) pero mantenir els del menu de servei
|
||||
// (Group::INTERFACE) i la música. En restaurar, reaplica el volum efectiu
|
||||
// normal del canal (que ja respecta els gates master/sound).
|
||||
void silenceGroup(Group group, bool silenced);
|
||||
|
||||
// --- Configuración de música ---
|
||||
void enableMusic(bool value); // Estableix l'estat de la música (reaplica volum)
|
||||
|
||||
@@ -46,7 +46,7 @@ namespace Ja {
|
||||
};
|
||||
|
||||
// --- Constants ---
|
||||
inline constexpr int MAX_SIMULTANEOUS_CHANNELS = 20;
|
||||
inline constexpr int MAX_SIMULTANEOUS_CHANNELS = 50;
|
||||
inline constexpr int MAX_GROUPS = 2;
|
||||
// Cap superior de canals que poden estar simultàniament reproduint un so
|
||||
// con efecte (eco/reverb). Si está al límit, las noves crides con efecte
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
namespace Config {
|
||||
@@ -49,32 +48,36 @@ namespace Config {
|
||||
int button_right{SDL_GAMEPAD_BUTTON_DPAD_RIGHT};
|
||||
int button_thrust{SDL_GAMEPAD_BUTTON_WEST}; // X button
|
||||
int button_shoot{SDL_GAMEPAD_BUTTON_SOUTH}; // A button
|
||||
int button_start{SDL_GAMEPAD_BUTTON_START}; // Start button
|
||||
int button_menu{SDL_GAMEPAD_BUTTON_BACK}; // Select/Back -> obre menu servei
|
||||
};
|
||||
|
||||
struct PlayerBindings {
|
||||
KeyboardBindings keyboard{};
|
||||
GamepadBindings gamepad{};
|
||||
std::string gamepad_name; // Empty = auto-assign by index
|
||||
std::string gamepad_path; // Prioritari sobre name per distingir mateixos models
|
||||
};
|
||||
|
||||
struct AudioConfig {
|
||||
bool enabled{true};
|
||||
float volume{1.0F}; // Master 0..1
|
||||
bool music_enabled{true};
|
||||
float music_volume{1.0F};
|
||||
bool sound_enabled{true};
|
||||
float sound_volume{0.25F};
|
||||
};
|
||||
|
||||
struct EngineConfig {
|
||||
WindowConfig window{};
|
||||
RenderingConfig rendering{};
|
||||
AudioConfig audio{};
|
||||
PlayerBindings player1{};
|
||||
PlayerBindings player2{};
|
||||
KeyboardBindings keyboard_controls{}; // Defaults globals per Input
|
||||
GamepadBindings gamepad_controls{};
|
||||
bool console{false};
|
||||
};
|
||||
|
||||
// Capa de persistència delegada cap a l'EngineConfig. Permet al Director
|
||||
// orquestrar init/load/save sense conèixer cap esquema concret (YAML,
|
||||
// SQLite, ...) ni la capa que el conté (`game/config_yaml.cpp`).
|
||||
struct ConfigPersistence {
|
||||
std::function<void()> init_defaults; // Restaura valors per defecte
|
||||
std::function<void(const std::string& path)> set_path; // Indica on guardar
|
||||
std::function<bool()> load; // Llegeix path → EngineConfig
|
||||
std::function<bool()> save; // Escriu EngineConfig → path
|
||||
std::string locale{"ca"}; // "ca" | "en" — fixat a l'arrencada, sense hot-swap
|
||||
};
|
||||
|
||||
} // namespace Config
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
#include "core/defaults/physics.hpp"
|
||||
#include "core/defaults/playfield.hpp"
|
||||
#include "core/defaults/rendering.hpp"
|
||||
#include "core/defaults/ship.hpp"
|
||||
#include "core/defaults/starfield_parallax.hpp"
|
||||
#include "core/defaults/title.hpp"
|
||||
#include "core/defaults/trail.hpp"
|
||||
#include "core/defaults/window.hpp"
|
||||
|
||||
@@ -35,10 +35,12 @@ namespace Defaults::Music {
|
||||
namespace Defaults::Sound {
|
||||
|
||||
constexpr const char* CONTINUE = "effects/continue.wav"; // Cuenta atras
|
||||
constexpr const char* EXPLOSION = "effects/explosion.wav"; // Explosión
|
||||
constexpr const char* EXPLOSION2 = "effects/explosion2.wav"; // Explosión alternativa
|
||||
constexpr const char* ENEMY_EXPLOSION = "effects/enemy_explosion.wav"; // Explosió d'enemic (debris default)
|
||||
constexpr const char* ENEMY_HIT = "effects/enemy_hit.wav"; // Impacte parcial a enemic (debris_partial — HP > 1)
|
||||
constexpr const char* PLAYER_EXPLOSION = "effects/player_explosion.wav"; // Explosió de la nau del jugador
|
||||
constexpr const char* FRIENDLY_FIRE_HIT = "effects/friendly_fire.wav"; // Friendly fire hit
|
||||
constexpr const char* HIT = "effects/hit.wav"; // Enemic ferit (primer impacte → HURT)
|
||||
constexpr const char* BULLET_ZAP = "effects/bullet_zap.wav"; // Bala desintegrant-se (qualsevol impacte o eixida de playarea)
|
||||
constexpr const char* HURT = "effects/hurt.wav"; // Nau pròpia entra a HURT
|
||||
constexpr const char* INIT_HUD = "effects/init_hud.wav"; // Para la animación del HUD
|
||||
constexpr const char* LASER = "effects/laser_shoot.wav"; // Disparo
|
||||
constexpr const char* LOGO = "effects/logo.wav"; // Logo
|
||||
|
||||
@@ -5,6 +5,48 @@
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
namespace Defaults::FX::Glow {
|
||||
|
||||
// Neon glow per outline gruixut, aplicat automàticament per renderShape.
|
||||
// Els gruixos d'halo són RÀTIOS del bounding_radius de la shape (escalat
|
||||
// per scale), de manera que un pentàgon (radius 20) té halo gros i una bala
|
||||
// (radius 3) té halo subtil. El core (últim pass) usa el gruix de línia
|
||||
// global (1.5px) — no escala amb la shape.
|
||||
//
|
||||
// Cap superior: si la shape és molt gran (logos del títol, intro), el
|
||||
// bounding_radius es satura a aquest valor — així cap shape té més
|
||||
// glow que el pentàgon (referència de gameplay).
|
||||
constexpr float MAX_REFERENCE_RADIUS = 20.0F;
|
||||
|
||||
struct Pass {
|
||||
float thickness_ratio; // % del bounding_radius*scale. <0 → usa core (gruix global)
|
||||
float alpha;
|
||||
};
|
||||
constexpr Pass PASSES[] = {
|
||||
{.thickness_ratio = 0.55F, .alpha = 0.07F},
|
||||
{.thickness_ratio = 0.35F, .alpha = 0.14F},
|
||||
{.thickness_ratio = 0.20F, .alpha = 0.28F},
|
||||
{.thickness_ratio = -1.0F, .alpha = 1.0F}, // core: línia "real"
|
||||
};
|
||||
|
||||
// Glow per a línies "raw" (sense shape). Gruixos absoluts (px), no
|
||||
// ratios — una línia individual no té bounding radius. Útil per a
|
||||
// partícules de firework, sparks, etc.
|
||||
namespace Line {
|
||||
struct Pass {
|
||||
float thickness; // px. <0 → usa el thickness passat pel caller (core)
|
||||
float alpha;
|
||||
};
|
||||
constexpr Pass PASSES[] = {
|
||||
{.thickness = 18.0F, .alpha = 0.10F},
|
||||
{.thickness = 12.0F, .alpha = 0.20F},
|
||||
{.thickness = 6.0F, .alpha = 0.40F},
|
||||
{.thickness = -1.0F, .alpha = 1.0F}, // core: línia "real"
|
||||
};
|
||||
} // namespace Line
|
||||
|
||||
} // namespace Defaults::FX::Glow
|
||||
|
||||
namespace Defaults::FX::Firework {
|
||||
|
||||
// Color per defecte. La caller pot fer override (p.ex. heretar del pare),
|
||||
|
||||
@@ -1,101 +1,45 @@
|
||||
// enemies.hpp - Configuració per tipus d'enemic (Pentagon/Cuadrado/Molinillo), spawn i scoring
|
||||
// enemies.hpp - Constants tècniques compartides per al sistema d'enemics.
|
||||
// © 2026 JailDesigner
|
||||
//
|
||||
// Tots els paràmetres jugables (physics, animation, wounded, spawn,
|
||||
// behavior, colors, scoring) viuen a data/entities/<type>/<type>.yaml i
|
||||
// s'accedeixen via EnemyRegistry::get(EnemyType). Aquí només queda el
|
||||
// que no és per personalitzar per tipus.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "core/defaults/entities.hpp"
|
||||
namespace Defaults::Enemies::Spawn {
|
||||
|
||||
namespace Defaults::Enemies {
|
||||
// Sostre de reintents al cercar una posició de spawn que respecti el
|
||||
// safety_distance del tipus. No és un paràmetre jugable: és el llindar
|
||||
// tècnic abans de caure a un fallback aleatori amb advertència.
|
||||
constexpr int MAX_SPAWN_ATTEMPTS = 50;
|
||||
|
||||
// Cuerpo físico común (valores por defecto del constructor)
|
||||
namespace Body {
|
||||
constexpr float DEFAULT_MASS = 5.0F; // Más liviano que la nave (10.0)
|
||||
constexpr float RESTITUTION = 1.0F; // Rebote elástico perfecto contra paredes
|
||||
constexpr float LINEAR_DAMPING = 0.0F; // Sin fricción: mantienen velocidad
|
||||
constexpr float ANGULAR_DAMPING = 0.0F;
|
||||
} // namespace Body
|
||||
} // namespace Defaults::Enemies::Spawn
|
||||
|
||||
// Pentagon (esquivador - zigzag evasion)
|
||||
namespace Pentagon {
|
||||
constexpr float VELOCITAT = 35.0F; // px/s (slightly slower)
|
||||
constexpr float MASS = 5.0F; // Masa estándar
|
||||
constexpr float CANVI_ANGLE_PROB = 0.20F; // 20% per wall hit (frequent zigzag)
|
||||
constexpr float CANVI_ANGLE_MAX = 1.0F; // Max random angle change (rad)
|
||||
constexpr float ZIGZAG_PROB_PER_SECOND = 0.8F; // Probabilidad de zigzag por segundo
|
||||
constexpr float DROTACIO_MIN = 0.75F; // Min visual rotation (rad/s) [+50%]
|
||||
constexpr float DROTACIO_MAX = 3.75F; // Max visual rotation (rad/s) [+50%]
|
||||
constexpr const char* SHAPE_FILE = "enemy_pentagon.shp";
|
||||
} // namespace Pentagon
|
||||
namespace Defaults::Enemies::Visual {
|
||||
|
||||
// Cuadrado (perseguidor - tracks player)
|
||||
namespace Cuadrado {
|
||||
constexpr float VELOCITAT = 40.0F; // px/s (medium speed)
|
||||
constexpr float MASS = 8.0F; // Más pesado, "tanque"
|
||||
constexpr float TRACKING_STRENGTH = 0.5F; // Interpolation toward player (0.0-1.0)
|
||||
constexpr float TRACKING_INTERVAL = 1.0F; // Seconds between angle updates
|
||||
constexpr float DROTACIO_MIN = 0.3F; // Slow rotation [+50%]
|
||||
constexpr float DROTACIO_MAX = 1.5F; // [+50%]
|
||||
constexpr const char* SHAPE_FILE = "enemy_square.shp";
|
||||
} // namespace Cuadrado
|
||||
// Duració del "flash" que dispara l'acció FLASH (feedback per impacte
|
||||
// parcial en enemics HP>1). Curt: l'efecte ha de llegir-se com un cop,
|
||||
// no com una transició.
|
||||
constexpr float FLASH_DURATION = 0.08F;
|
||||
|
||||
// Molinillo (agressiu - fast straight lines, proximity spin-up)
|
||||
namespace Molinillo {
|
||||
constexpr float VELOCITAT = 50.0F; // px/s (fastest)
|
||||
constexpr float MASS = 4.0F; // Más liviano, ágil
|
||||
constexpr float CANVI_ANGLE_PROB = 0.05F; // 5% per wall hit (rare direction change)
|
||||
constexpr float CANVI_ANGLE_MAX = 0.3F; // Small angle adjustments
|
||||
constexpr float DROTACIO_MIN = 3.0F; // Base rotation (rad/s) [+50%]
|
||||
constexpr float DROTACIO_MAX = 6.0F; // [+50%]
|
||||
constexpr float DROTACIO_PROXIMITY_MULTIPLIER = 3.0F; // Spin-up multiplier when near ship
|
||||
constexpr float PROXIMITY_DISTANCE = 100.0F; // Distance threshold (px)
|
||||
constexpr const char* SHAPE_FILE = "enemy_pinwheel.shp";
|
||||
} // namespace Molinillo
|
||||
} // namespace Defaults::Enemies::Visual
|
||||
|
||||
// Animation parameters (shared)
|
||||
namespace Animation {
|
||||
// Palpitation
|
||||
constexpr float PALPITACIO_TRIGGER_PROB = 0.01F; // 1% chance per second
|
||||
constexpr float PALPITACIO_DURACIO_MIN = 1.0F; // Min duration (seconds)
|
||||
constexpr float PALPITACIO_DURACIO_MAX = 3.0F; // Max duration (seconds)
|
||||
constexpr float PALPITACIO_AMPLITUD_MIN = 0.08F; // Min scale variation
|
||||
constexpr float PALPITACIO_AMPLITUD_MAX = 0.20F; // Max scale variation
|
||||
constexpr float PALPITACIO_FREQ_MIN = 1.5F; // Min frequency (Hz)
|
||||
constexpr float PALPITACIO_FREQ_MAX = 3.0F; // Max frequency (Hz)
|
||||
namespace Defaults::Enemies::Debris {
|
||||
|
||||
// Rotation acceleration
|
||||
constexpr float ROTACIO_ACCEL_TRIGGER_PROB = 0.02F; // 2% chance per second [4x more frequent]
|
||||
constexpr float ROTACIO_ACCEL_DURACIO_MIN = 3.0F; // Min transition time
|
||||
constexpr float ROTACIO_ACCEL_DURACIO_MAX = 8.0F; // Max transition time
|
||||
constexpr float ROTACIO_ACCEL_MULTIPLIER_MIN = 0.3F; // Min speed multiplier [more dramatic]
|
||||
constexpr float ROTACIO_ACCEL_MULTIPLIER_MAX = 4.0F; // Max speed multiplier [more dramatic]
|
||||
} // namespace Animation
|
||||
// Escala dels fragments per a l'acció CREATE_DEBRIS_PARTIAL (xip d'impacte
|
||||
// en enemics HP>1). 0.3 = trossos petits, com de "casc esquerdat".
|
||||
constexpr float PARTIAL_PIECE_SCALE = 0.3F;
|
||||
|
||||
// Wounded state (entre primer impacto y explosión)
|
||||
namespace Wounded {
|
||||
constexpr float DURATION = 1.0F; // Segundos en estado herido antes de explotar
|
||||
constexpr float BLINK_HZ = 10.0F; // Frecuencia de parpadeo color tipo ↔ dorado
|
||||
} // namespace Wounded
|
||||
} // namespace Defaults::Enemies::Debris
|
||||
|
||||
// Spawn safety and invulnerability system
|
||||
namespace Spawn {
|
||||
// Safe spawn distance from player
|
||||
constexpr float SAFETY_DISTANCE_MULTIPLIER = 3.0F; // 3x ship radius
|
||||
constexpr float SAFETY_DISTANCE = Defaults::Entities::SHIP_RADIUS * SAFETY_DISTANCE_MULTIPLIER; // 36.0f px
|
||||
constexpr int MAX_SPAWN_ATTEMPTS = 50; // Max attempts to find safe position
|
||||
namespace Defaults::Enemies::Fireworks {
|
||||
|
||||
// Invulnerability system
|
||||
constexpr float INVULNERABILITY_DURATION = 3.0F; // Seconds
|
||||
constexpr float INVULNERABILITY_BRIGHTNESS_START = 0.3F; // Dim
|
||||
constexpr float INVULNERABILITY_BRIGHTNESS_END = 0.7F; // Normal (same as Defaults::Brightness::ENEMIC)
|
||||
constexpr float INVULNERABILITY_SCALE_START = 0.0F; // Invisible
|
||||
constexpr float INVULNERABILITY_SCALE_END = 1.0F; // Full size
|
||||
} // namespace Spawn
|
||||
// Paràmetres del firework "petit" per a l'acció CREATE_FIREWORKS_SMALL
|
||||
// (feedback per impacte parcial en enemics HP>1). Pocs punts i baixa
|
||||
// velocitat: una espurna breu, no una explosió.
|
||||
constexpr int SMALL_N_POINTS = 20;
|
||||
constexpr float SMALL_SPEED = 250.0F;
|
||||
|
||||
// Scoring system (puntuación per type de enemy)
|
||||
namespace Scoring {
|
||||
constexpr int PENTAGON_SCORE = 100; // Pentágono (esquivador, 35 px/s)
|
||||
constexpr int QUADRAT_SCORE = 150; // Cuadrado (perseguidor, 40 px/s)
|
||||
constexpr int MOLINILLO_SCORE = 200; // Molinillo (agressiu, 50 px/s)
|
||||
} // namespace Scoring
|
||||
|
||||
} // namespace Defaults::Enemies
|
||||
} // namespace Defaults::Enemies::Fireworks
|
||||
|
||||
@@ -3,13 +3,28 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Defaults::Entities {
|
||||
|
||||
constexpr int MAX_ORNIS = 15;
|
||||
constexpr int MAX_BALES = 50;
|
||||
constexpr int MAX_BULLETS = 50; // per jugador (P1 + P2 = 2× aquest valor)
|
||||
constexpr int MAX_ENEMY_BULLETS = 50; // pool reservat per a bales d'enemic
|
||||
|
||||
constexpr float SHIP_RADIUS = 12.0F;
|
||||
constexpr float ENEMY_RADIUS = 20.0F;
|
||||
constexpr float BULLET_RADIUS = 3.0F;
|
||||
// Total real de slots a l'array global bullets_: zona P1, zona P2 i zona enemic.
|
||||
// Reservar zones impedeix que les bales d'enemic ocupin slots del jugador.
|
||||
constexpr int MAX_BULLETS_TOTAL = (MAX_BULLETS * 2) + MAX_ENEMY_BULLETS;
|
||||
constexpr int ENEMY_BULLET_START_IDX = MAX_BULLETS * 2;
|
||||
|
||||
// Convenció d'owner_id per a Bullet::fire:
|
||||
// 0..1 = players (P1, P2)
|
||||
// ENEMY_OWNER_BASE + index = enemic concret (per identificar el seu autoimpacte)
|
||||
// Una bala mata a qualsevol col·lisió excepte amb el seu propi creador.
|
||||
constexpr uint8_t ENEMY_OWNER_BASE = 128;
|
||||
|
||||
// SHIP_RADIUS / ENEMY_RADIUS / BULLET_RADIUS han migrat: ara cada entitat
|
||||
// calcula el seu collision_radius com a
|
||||
// shape.bounding_radius × shape.scale × shape.collision_factor
|
||||
// a partir del seu YAML (data/entities/<name>/<name>.yaml).
|
||||
|
||||
} // namespace Defaults::Entities
|
||||
|
||||
@@ -10,21 +10,27 @@ namespace Defaults::Game {
|
||||
constexpr int HEIGHT = 720;
|
||||
|
||||
// Regles de partida
|
||||
constexpr int STARTING_LIVES = 3; // Initial lives
|
||||
constexpr int MAX_VIDES = 3; // Vides màximes per jugador (font única; el HUD en deriva els slots)
|
||||
constexpr int STARTING_LIVES = MAX_VIDES; // S'arrenca amb les vides al màxim
|
||||
constexpr float DEATH_DURATION = 3.0F; // Seconds of death animation
|
||||
constexpr float GAME_OVER_DURATION = 5.0F; // Seconds to display game over
|
||||
|
||||
// Valores centinela del temporitzador de mort per-jugador.
|
||||
constexpr float HIT_TIMER_INACTIVE_PLAYER = 999.0F; // Jugador permanentment inactiu
|
||||
constexpr float HIT_TIMER_TRIGGER_DEATH = 0.001F; // Trigger inicial post-impacte (>0 sense disparar regla)
|
||||
constexpr float COLLISION_SHIP_ENEMY_AMPLIFIER = 0.80F; // 80% hitbox (generous)
|
||||
// Ha de ser ≥ 1.0F: PhysicsWorld separa els cossos al contacte exacte (dist == suma de radis),
|
||||
// així que un amplificador < 1 fa que el check de gameplay no es dispari mai. Marge petit
|
||||
// (1.05F) per tolerar floating-point i petites separacions post-impuls.
|
||||
constexpr float COLLISION_SHIP_ENEMY_AMPLIFIER = 1.05F;
|
||||
constexpr float COLLISION_BULLET_ENEMY_AMPLIFIER = 1.15F; // 115% hitbox (generous)
|
||||
// Wounded chain: el rebot físic separa els cossos abans que arribi
|
||||
// la detecció gameplay; amplier generós perquè el toc compti.
|
||||
constexpr float COLLISION_WOUNDED_CHAIN_AMPLIFIER = 1.25F;
|
||||
|
||||
// Friendly fire system
|
||||
constexpr bool FRIENDLY_FIRE_ENABLED = true; // Activar friendly fire
|
||||
constexpr float COLLISION_BULLET_PLAYER_AMPLIFIER = 1.0F; // Hitbox exacto (100%)
|
||||
constexpr float BULLET_GRACE_PERIOD = 0.2F; // Inmunidad post-disparo (s)
|
||||
constexpr float BULLET_SPEED = 700.0F; // Velocidad escalar (px/s). Pascal: 7 px/frame × 20 FPS
|
||||
// BULLET_SPEED migrat a data/entities/player/player.yaml (weapon.bullet_speed).
|
||||
|
||||
// Transición LEVEL_START (mensajes aleatorios PRE-level)
|
||||
constexpr float LEVEL_START_DURATION = 3.0F; // Duración total
|
||||
@@ -34,6 +40,19 @@ namespace Defaults::Game {
|
||||
constexpr float LEVEL_COMPLETED_DURATION = 3.0F; // Duración total
|
||||
constexpr float LEVEL_COMPLETED_TYPING_RATIO = 0.05F; // ~150ms de typewriter (escan ràpid però visible)
|
||||
|
||||
// Attract mode: transició TÍTOL → DEMO. Primer un "dive" de càmera cap al
|
||||
// punt de fuga (deixa enrere títol/naus/logo) i després una cortinilla negra
|
||||
// que cau per tapar; a la demo, la cortinilla segueix caient i destapa.
|
||||
namespace Dive {
|
||||
constexpr float DURATION = 0.55F; // Durada del dive (s), amb acceleració (ease-in)
|
||||
constexpr float CAMERA_DISTANCE = 450.0F; // Avanç de la càmera en +Z (passa les naus, a Z≈323)
|
||||
constexpr float ZOOM_MAX = 7.0F; // Zoom final dels elements 2D (logo + peu) en travessar-los
|
||||
} // namespace Dive
|
||||
namespace Curtain {
|
||||
constexpr float COVER_DURATION = 0.35F; // TÍTOL: la tela cau i tapa
|
||||
constexpr float REVEAL_DURATION = 0.45F; // DEMO: la tela segueix caient i destapa
|
||||
} // namespace Curtain
|
||||
|
||||
// Transición INIT_HUD (animación inicial del HUD)
|
||||
constexpr float INIT_HUD_DURATION = 3.0F; // Duración total del estado
|
||||
|
||||
@@ -54,7 +73,7 @@ namespace Defaults::Game {
|
||||
constexpr float INIT_HUD_SHIP2_RATIO_INIT = 0.20F;
|
||||
constexpr float INIT_HUD_SHIP2_RATIO_END = 1.0F;
|
||||
|
||||
// Posición inicial de la nave en INIT_HUD (75% de altura de zona de juego)
|
||||
// Posición inicial de la nave en INIT_HUD (75% de altura de zone de juego)
|
||||
constexpr float INIT_HUD_SHIP_START_Y_RATIO = 0.75F; // 75% desde el top de PLAYAREA
|
||||
|
||||
// Spawn positions (distribución horizontal para 2 jugadores)
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Defaults::Hud {
|
||||
|
||||
// Marcador (scoreboard inferior). Usado por GameScene::drawScoreboard()
|
||||
@@ -12,6 +14,42 @@ namespace Defaults::Hud {
|
||||
constexpr float SCOREBOARD_TEXT_SCALE = 0.85F;
|
||||
constexpr float SCOREBOARD_TEXT_SPACING = 0.0F;
|
||||
|
||||
// Mode de presentació de les vides al marcador (no es canvia en calent;
|
||||
// es defineix ací mentre no estiga decidit si el nombre de vides serà fix).
|
||||
// SLOTS → naus en miniatura en posicions fixes (s'encenen/atenuen).
|
||||
// DIGITS → número de 2 dígits (mateixa regla que el nivell: zeros a
|
||||
// l'esquerra atenuats, dígit significatiu en endavant encès).
|
||||
enum class LivesDisplay : std::uint8_t { SLOTS,
|
||||
DIGITS };
|
||||
constexpr LivesDisplay LIVES_DISPLAY = LivesDisplay::DIGITS;
|
||||
|
||||
// Ajust fi de l'alçada dels slots de vides respecte a l'alçada del glif del
|
||||
// dígit: la silueta de la nau ompli menys que un dígit, així que un xicotet
|
||||
// factor >1 la fa casar visualment amb les xifres (calibrat a ull).
|
||||
constexpr float LIFE_SLOT_HEIGHT_FACTOR = 1.2F;
|
||||
|
||||
// Esquema de color del marcador: "per jugador + sistema". Cada jugador usa
|
||||
// el SEU color (parella brillant/atenuat) en tot el seu bloc (punts + vides);
|
||||
// el nivell central va sempre en verd de sistema. Colors plans i purs: el
|
||||
// glow/bloom el posa el shader de postpro, NO s'horneja al color. Amb
|
||||
// alpha=255 el line_renderer usa el color directament sense caure al fallback
|
||||
// verd (Rendering::DEFAULT_LINE_COLOR).
|
||||
namespace Colors {
|
||||
// Jugador 1 → cian.
|
||||
constexpr SDL_Color P1_BRIGHT = {.r = 41, .g = 231, .b = 255, .a = 255}; // #29E7FF
|
||||
constexpr SDL_Color P1_DIM = {.r = 12, .g = 90, .b = 102, .a = 255}; // #0C5A66
|
||||
// Jugador 2 → groc.
|
||||
constexpr SDL_Color P2_BRIGHT = {.r = 255, .g = 226, .b = 58, .a = 255}; // #FFE23A
|
||||
constexpr SDL_Color P2_DIM = {.r = 90, .g = 82, .b = 16, .a = 255}; // #5A5210
|
||||
// Nivell / sistema → verd.
|
||||
constexpr SDL_Color LEVEL_BRIGHT = {.r = 77, .g = 255, .b = 102, .a = 255}; // #4DFF66
|
||||
constexpr SDL_Color LEVEL_DIM = {.r = 29, .g = 107, .b = 44, .a = 255}; // #1D6B2C
|
||||
} // namespace Colors
|
||||
|
||||
// Les vides es dibuixen com a slots fixos de naus en miniatura (NUM_SLOTS =
|
||||
// MAX_VIDES − 1). Mida i pas dels slots es deriven de la mètrica del glif del
|
||||
// dígit a init_hud_animator, no de constants soltes.
|
||||
|
||||
// Animación de entrada del HUD (init_hud_animator).
|
||||
namespace InitAnim {
|
||||
// Spawn vertical de la nave: 50 px bajo la PLAYAREA (sale desde fuera).
|
||||
@@ -33,7 +71,9 @@ namespace Defaults::Hud {
|
||||
namespace DebugOverlay {
|
||||
constexpr float X = 30.0F;
|
||||
constexpr float Y_FPS = 24.0F;
|
||||
constexpr float FPS_LINE_HEIGHT = 28.0F; // separació després del FPS (scale 0.7 → ~28 px)
|
||||
constexpr float LINE_HEIGHT = 18.0F; // separació entre línies (scale 0.4 → ~16 px alt)
|
||||
constexpr float FPS_SCALE = 0.7F; // FPS més gran que la resta
|
||||
constexpr float TEXT_SCALE = 0.4F;
|
||||
constexpr float TEXT_SPACING = 2.0F;
|
||||
constexpr float BRIGHTNESS = 1.0F;
|
||||
|
||||
@@ -6,19 +6,18 @@
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
// Paleta semántica por tipo de entidad. Si una entity declara color, lo
|
||||
// pasa al pipeline con alpha=255 (sentinela "color válido"); si no, se
|
||||
// usa el color global del oscilador (g_current_line_color).
|
||||
// pasa al pipeline con alpha=255 (sentinela "color válido"); si no,
|
||||
// line_renderer::linea() cau a DEFAULT_LINE_COLOR (verd fòsfor fallback).
|
||||
namespace Defaults::Palette {
|
||||
|
||||
// Paleta neon: pujada lleugera dels canals secundaris per millorar la
|
||||
// brillantor perceptual sota el bloom (sense alterar la identitat de color).
|
||||
// El canal dominant es manté a 255 a cada color per maximitzar la saturació
|
||||
// visible quan el halo s'expandeix.
|
||||
constexpr SDL_Color SHIP = {.r = 255, .g = 255, .b = 255, .a = 255}; // Blanco neutro
|
||||
constexpr SDL_Color BULLET = {.r = 155, .g = 255, .b = 175, .a = 255}; // Verde laser
|
||||
constexpr SDL_Color PENTAGON = {.r = 155, .g = 195, .b = 255, .a = 255}; // Azul "esquivador"
|
||||
constexpr SDL_Color QUADRAT = {.r = 255, .g = 140, .b = 140, .a = 255}; // Rojo "tank"
|
||||
constexpr SDL_Color MOLINILLO = {.r = 255, .g = 160, .b = 255, .a = 255}; // Magenta agresivo
|
||||
constexpr SDL_Color WOUNDED = {.r = 255, .g = 220, .b = 60, .a = 255}; // Dorado: enemigo herido
|
||||
// Tots els colors d'entitats han migrat al seu YAML respectiu
|
||||
// (data/entities/<name>/<name>.yaml, secció `colors`):
|
||||
// - SHIP → player.yaml
|
||||
// - PENTAGON / SQUARE / PINWHEEL / WOUNDED → cada enemy.yaml
|
||||
// - BULLET → bullet.yaml
|
||||
|
||||
} // namespace Defaults::Palette
|
||||
|
||||
@@ -3,28 +3,20 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace Defaults::Physics {
|
||||
// NOTA: els paràmetres del player (rotation_speed, acceleration,
|
||||
// max_velocity, death_impact_factor) viuen a data/entities/player/player.yaml.
|
||||
// Els paràmetres específics de la bala (mass, restitution, damping,
|
||||
// impact_momentum_factor) viuen a data/entities/bullet/bullet.yaml.
|
||||
// Aquest fitxer només conté els paràmetres compartits del subsistema de
|
||||
// debris (explosions visuals).
|
||||
|
||||
constexpr float ROTATION_SPEED = 3.14F; // rad/s (~180°/s)
|
||||
constexpr float ACCELERATION = 400.0F; // px/s²
|
||||
constexpr float MAX_VELOCITY = 180.0F; // px/s
|
||||
constexpr float FRICTION = 20.0F; // px/s²
|
||||
namespace Defaults::Physics::Debris {
|
||||
|
||||
// Bullet — impacto físico contra enemigo (impulse mass-aware).
|
||||
// Model: el impulse és el moment lineal de la bala (m·v) multiplicat per
|
||||
// un factor de transferència [0..1]. 1.0 = transfereix tot el moment
|
||||
// (col·lisió perfectament inelàstica), 0.5 = transfereix la meitat.
|
||||
namespace Bullet {
|
||||
constexpr float IMPACT_MOMENTUM_FACTOR = 3.0F; // Factor de transferència de moment bala→enemic
|
||||
} // namespace Bullet
|
||||
|
||||
// Explosions (debris physics)
|
||||
namespace Debris {
|
||||
constexpr float VELOCITAT_BASE = 80.0F; // Velocidad inicial (px/s)
|
||||
constexpr float VARIACIO_VELOCITAT = 40.0F; // ±variació aleatòria (px/s)
|
||||
constexpr float SPEED_BASE = 80.0F; // Velocidad inicial (px/s)
|
||||
constexpr float VARIACIO_SPEED = 40.0F; // ±variació aleatòria (px/s)
|
||||
constexpr float ACCELERACIO = -60.0F; // Fricció/desacceleració (px/s²)
|
||||
constexpr float ROTACIO_MIN = 0.1F; // Rotación mínima (rad/s ~5.7°/s)
|
||||
constexpr float ROTACIO_MAX = 0.3F; // Rotación màxima (rad/s ~17.2°/s)
|
||||
constexpr float ROTATION_MIN = 0.1F; // Rotación mínima (rad/s ~5.7°/s)
|
||||
constexpr float ROTATION_MAX = 0.3F; // Rotación màxima (rad/s ~17.2°/s)
|
||||
constexpr float TEMPS_VIDA = 2.0F; // Vida mínima garantida (s) — després pot morir per velocitat baixa
|
||||
constexpr float TEMPS_VIDA_NAU = 3.0F; // Ship debris min lifetime (matches DEATH_DURATION)
|
||||
constexpr float SHRINK_RATE = 1.0F; // Reducció de mida (1.0 = encoge a 0 al final del min_lifetime)
|
||||
@@ -40,8 +32,8 @@ namespace Defaults::Physics {
|
||||
constexpr float RESTITUTION_BOUNDS = 0.7F;
|
||||
|
||||
// Herència de velocity angular (trayectorias curvas)
|
||||
constexpr float FACTOR_HERENCIA_MIN = 0.7F; // Mínimo 70% del drotacio heredat
|
||||
constexpr float FACTOR_HERENCIA_MAX = 1.0F; // Màxim 100% del drotacio heredat
|
||||
constexpr float INHERITANCE_FACTOR_MIN = 0.7F; // Mínimo 70% del drotacio heredat
|
||||
constexpr float INHERITANCE_FACTOR_MAX = 1.0F; // Màxim 100% del drotacio heredat
|
||||
constexpr float FRICCIO_ANGULAR = 0.5F; // Desacceleració angular (rad/s²)
|
||||
|
||||
// Velocity heredada de la nau a l'explosió (80% del feel original).
|
||||
@@ -51,6 +43,13 @@ namespace Defaults::Physics {
|
||||
// 1.0 = inèrcia completa; >1.0 amplifica la deriva; <1.0 la atenua.
|
||||
constexpr float ENEMY_VELOCITY_INHERITANCE = 1.0F;
|
||||
|
||||
// Velocitat de la bala traspassada a cada fragment de debris al moment
|
||||
// de l'impacte. Separat de la inèrcia del cos (velocitat_objecte): permet
|
||||
// que els trossos volin "amb la força de la bala" encara que el cos pesi
|
||||
// molt i amb prou feines es mogui. 0.4 a 700 px/s = ~280 px/s extra per
|
||||
// fragment, molt visible sense ser excessiu.
|
||||
constexpr float BULLET_IMPULSE_FACTOR = 0.4F;
|
||||
|
||||
// Tuneig específic de l'explosió d'enemic (overrides als defaults
|
||||
// que es passen com a paràmetres opcionals a explode()).
|
||||
constexpr float ENEMY_LIFETIME = 2.5F; // Vida mínima del debris (s) — els que segueixen movent-se viuen més
|
||||
@@ -60,7 +59,6 @@ namespace Defaults::Physics {
|
||||
// Angular velocity sin for trajectory inheritance
|
||||
// Excess above this threshold is converted to tangential linear velocity
|
||||
// Prevents "vortex trap" problem with high-rotation enemies
|
||||
constexpr float VELOCITAT_ROT_MAX = 1.5F; // rad/s (~86°/s)
|
||||
} // namespace Debris
|
||||
constexpr float SPEED_ROT_MAX = 1.5F; // rad/s (~86°/s)
|
||||
|
||||
} // namespace Defaults::Physics
|
||||
} // namespace Defaults::Physics::Debris
|
||||
|
||||
@@ -3,16 +3,21 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
namespace Defaults::Playfield {
|
||||
|
||||
// Estructura de la graella (cel·les omplen tota la PLAYAREA)
|
||||
constexpr int COLUMNS = 16; // cell_w = PLAYAREA.w / 16
|
||||
constexpr int ROWS = 8; // cell_h = PLAYAREA.h / 8
|
||||
constexpr int SUBDIVISIONS = 5; // cada cel·la principal es divideix en N subcel·les
|
||||
constexpr int SUBDIVISIONS = 4; // cada cel·la principal es divideix en N subcel·les
|
||||
|
||||
// Brillo respecte al color global (border = 1.0)
|
||||
constexpr float GRID_BRIGHTNESS = 0.15F;
|
||||
constexpr float SUBGRID_BRIGHTNESS = 0.05F;
|
||||
constexpr float GRID_BRIGHTNESS = 0.20F;
|
||||
constexpr float SUBGRID_BRIGHTNESS = 0.10F;
|
||||
|
||||
// Color de la rejilla (lila/violeta synthwave). Es modula amb brillantor.
|
||||
constexpr SDL_Color GRID_COLOR = {.r = 160, .g = 80, .b = 255, .a = 255};
|
||||
|
||||
// Animació de creació amb timer intern del Playfield.
|
||||
// L'animació total cobreix tot l'INIT_HUD (3 s). Cada línia es pinta en
|
||||
@@ -25,20 +30,32 @@ namespace Defaults::Playfield {
|
||||
constexpr float HEAD_LENGTH_PX = 8.0F; // longitud en píxels lògics del tram brillant
|
||||
constexpr float HEAD_BRIGHTNESS = 0.0F; // brillo del cap (= border)
|
||||
|
||||
// Orbit (oscil·lació transversal de la línia quan la nau hi passa a prop).
|
||||
constexpr float ORBIT_AMPLITUDE_MAX_PX = 3.0F; // desplaçament transversal màxim
|
||||
constexpr float ORBIT_DECAY_PER_S = 4.0F; // decaiment de l'amplitud (px/s)
|
||||
constexpr float ORBIT_FREQ_HZ = 8.0F; // freqüència del sin
|
||||
constexpr float ORBIT_PROXIMITY_PX = 12.0F; // distància max de la línia per excitar-la
|
||||
constexpr float ORBIT_SHIP_SPEED_THRESHOLD = 60.0F; // velocitat mínima per excitar (px/s)
|
||||
// Ripples: deformacions circulars que travessen la graella com ones d'aigua.
|
||||
// Cada ripple desplaça radialment cap a fora els vèrtexs de les línies que
|
||||
// travessa, amb una envoltant que decau a les vores de l'anell i amb el temps.
|
||||
namespace Ripple {
|
||||
constexpr int POOL_SIZE = 32;
|
||||
|
||||
// Pulse (reacció a fireworks: punt brillant que es propaga al llarg de la
|
||||
// línia a partir del punt de spawn).
|
||||
constexpr int MAX_PULSES_PER_LINE = 2;
|
||||
constexpr float PULSE_LIFETIME_S = 1.0F; // temps total fins desaparèixer
|
||||
constexpr float PULSE_SPREAD_PER_S = 300.0F; // px/s de propagació (cap a cada extrem)
|
||||
constexpr unsigned char PULSE_COLOR_R = 180;
|
||||
constexpr unsigned char PULSE_COLOR_G = 230;
|
||||
constexpr unsigned char PULSE_COLOR_B = 255;
|
||||
// Ones grans (explosions / fireworks).
|
||||
constexpr float BIG_AMPLITUDE_PX = 10.0F;
|
||||
constexpr float BIG_SPEED_PX_S = 320.0F;
|
||||
constexpr float BIG_LIFETIME_S = 1.4F;
|
||||
constexpr float BIG_THICKNESS_PX = 40.0F;
|
||||
|
||||
// Ones petites (pas de nau, cadència estil trail).
|
||||
constexpr float SMALL_AMPLITUDE_PX = 2.5F;
|
||||
constexpr float SMALL_SPEED_PX_S = 160.0F;
|
||||
constexpr float SMALL_LIFETIME_S = 0.55F;
|
||||
constexpr float SMALL_THICKNESS_PX = 18.0F;
|
||||
|
||||
// Cadència "soltar gotetes" per nau (patró TrailManager).
|
||||
constexpr float SHIP_COOLDOWN_S = 0.10F;
|
||||
constexpr float SHIP_COOLDOWN_JITTER_S = 0.03F;
|
||||
constexpr float SHIP_SPEED_THRESHOLD_PX_S = 80.0F;
|
||||
|
||||
// Subdivisió de línies quan estan dins una ripple.
|
||||
constexpr int MAIN_SEGMENTS = 24; // línies principals
|
||||
constexpr int SUB_SEGMENTS = 12; // sub-graella
|
||||
} // namespace Ripple
|
||||
|
||||
} // namespace Defaults::Playfield
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
|
||||
namespace Defaults::Rendering {
|
||||
@@ -35,8 +34,12 @@ namespace Defaults::Rendering {
|
||||
constexpr int RENDER_HEIGHT_DEFAULT = 720;
|
||||
|
||||
constexpr auto isValidRenderResolution(int w, int h) -> bool {
|
||||
return std::ranges::any_of(RESOLUTION_PRESETS,
|
||||
[w, h](const ResolutionPreset& preset) { return preset.w == w && preset.h == h; });
|
||||
for (const auto& preset : RESOLUTION_PRESETS) {
|
||||
if (preset.w == w && preset.h == h) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace Defaults::Rendering
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
// service_menu.hpp - Constants del menu de servei (F12)
|
||||
// © 2026 JailDesigner
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
namespace Defaults::ServiceMenu {
|
||||
|
||||
// ---- Mides en coordenades logiques del joc (1280×720) ----
|
||||
// BOX_WIDTH_MIN es el minim: si el titol o algun item no hi caben, el
|
||||
// marc s'expandeix dinamicament amb animacio (cf. WIDTH_RATE).
|
||||
constexpr int BOX_WIDTH_MIN = 460;
|
||||
constexpr int GAP_Y = 22;
|
||||
constexpr int TITLE_HEIGHT = 36; // scale 0.85 ≈ 34 px de text
|
||||
constexpr int SUBTITLE_HEIGHT = 18; // scale 0.4 ≈ 16 px de text
|
||||
constexpr int SEPARATOR_HEIGHT = 1;
|
||||
constexpr int ITEM_HEIGHT = 38; // scale 0.55 ≈ 22 px de text + padding per al highlight
|
||||
constexpr int ITEM_GAP_Y = 6;
|
||||
|
||||
// Brackets als 4 cantons (substitueixen la vora completa: estètica sci-fi).
|
||||
constexpr int CORNER_ARM_H = 48;
|
||||
constexpr int CORNER_ARM_V = 28;
|
||||
constexpr int CORNER_THICKNESS = 2;
|
||||
|
||||
// ---- Animacio open/close (mateixos parametres que aee_arcade) ----
|
||||
constexpr float OPEN_SPEED = 8.0F; // ~125 ms a obrir
|
||||
constexpr float CLOSE_SPEED = 10.0F; // ~100 ms a tancar
|
||||
constexpr float HEIGHT_RATE = 12.0F; // smoothing exponencial de l'alçada de la caixa
|
||||
constexpr float WIDTH_RATE = 12.0F; // smoothing per a canvis d'ample entre pagines
|
||||
|
||||
// ---- Animacio del highlight (rectangle del cursor) ----
|
||||
// Rate=18 dona settling ~0.17 s al 95% (ease-out exponencial).
|
||||
constexpr float HIGHLIGHT_RATE = 18.0F;
|
||||
constexpr int HIGHLIGHT_TICK_LEN = 10; // longitud dels ticks a cada cantonada
|
||||
constexpr int HIGHLIGHT_THICKNESS = 1;
|
||||
constexpr int HIGHLIGHT_PAD_X = 18; // padding lateral del rect respecte al text
|
||||
constexpr int HIGHLIGHT_PAD_Y = 4; // padding vertical
|
||||
constexpr int TEXT_INSET_X = 16; // marge intern del text dins del highlight (label esq / valor dre)
|
||||
constexpr int MIN_LABEL_VALUE_GAP = 30; // mínim gap entre label i valor (per al càlcul d'ample dinàmic)
|
||||
|
||||
// ---- Colors RGBA ----
|
||||
constexpr SDL_Color BG_COLOR{.r = 0, .g = 12, .b = 24, .a = 215};
|
||||
constexpr SDL_Color CORNER_COLOR{.r = 120, .g = 220, .b = 255, .a = 255}; // cian neon
|
||||
constexpr SDL_Color TITLE_COLOR{.r = 200, .g = 240, .b = 255, .a = 255};
|
||||
constexpr SDL_Color SUBTITLE_COLOR{.r = 110, .g = 170, .b = 210, .a = 220}; // cian apagat
|
||||
constexpr SDL_Color SEPARATOR_COLOR{.r = 60, .g = 120, .b = 180, .a = 180};
|
||||
constexpr SDL_Color LABEL_COLOR{.r = 170, .g = 210, .b = 240, .a = 255};
|
||||
constexpr SDL_Color CURSOR_COLOR{.r = 255, .g = 230, .b = 120, .a = 255}; // groc per al text sel·leccionat
|
||||
constexpr SDL_Color HIGHLIGHT_OUTLINE{.r = 255, .g = 230, .b = 120, .a = 255}; // mateix groc, opac
|
||||
constexpr SDL_Color HIGHLIGHT_FILL{.r = 255, .g = 230, .b = 120, .a = 36}; // wash translucid
|
||||
|
||||
// ---- Tipografia (VectorText). Scale 1.0 = caracter 20×40 px ----
|
||||
constexpr float TITLE_SCALE = 0.85F; // mateixa escala que el HUD del scoreboard
|
||||
constexpr float SUBTITLE_SCALE = 0.40F; // sota el titol, info decorativa (versio/hash)
|
||||
constexpr float ITEM_SCALE = 0.55F; // mateixa escala que les notificacions
|
||||
constexpr float TEXT_SPACING = 2.0F;
|
||||
|
||||
// ---- Sons UI (relatius a data/sounds/), portats d'aee_arcade ----
|
||||
constexpr const char* SELECT_SOUND = "ui/menu_select.wav";
|
||||
constexpr const char* ACCEPT_SOUND = "ui/menu_accept.wav";
|
||||
|
||||
} // namespace Defaults::ServiceMenu
|
||||
@@ -1,27 +0,0 @@
|
||||
// ship.hpp - Configuració de la nau (invulnerabilitat, parpelleig)
|
||||
// © 2026 JailDesigner
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace Defaults::Ship {
|
||||
|
||||
// Invulnerabilidad post-respawn
|
||||
constexpr float INVULNERABILITY_DURATION = 3.0F; // Segundos de invulnerabilidad
|
||||
|
||||
// Parpadeo visual durante invulnerabilidad
|
||||
constexpr float BLINK_VISIBLE_TIME = 0.1F; // Tiempo visible (segundos)
|
||||
constexpr float BLINK_INVISIBLE_TIME = 0.1F; // Tiempo invisible (segundos)
|
||||
// Frecuencia total: 0.2s/ciclo = 5 Hz (~15 parpadeos en 3s)
|
||||
|
||||
// Cuerpo físico
|
||||
constexpr float MASS = 10.0F; // Masa de referencia para choques
|
||||
constexpr float RESTITUTION = 0.6F; // Rebote moderado contra paredes
|
||||
constexpr float LINEAR_DAMPING = 1.5F; // Fricción exponencial (s⁻¹)
|
||||
constexpr float ANGULAR_DAMPING = 0.0F; // Rotación 100% por input (no inercial)
|
||||
|
||||
// Empuje visual: escala proporcional a la velocidad (0..200 px/s → 1.0..1.5)
|
||||
// Mantiene la sensación del Pascal original.
|
||||
constexpr float VISUAL_PUSH_DIVISOR = 33.33F; // SPEED / DIVISOR = empuje visual
|
||||
constexpr float VISUAL_SCALE_DIVISOR = 12.0F; // SCALE = 1 + (PUSH / DIVISOR)
|
||||
|
||||
} // namespace Defaults::Ship
|
||||
@@ -0,0 +1,36 @@
|
||||
// starfield_parallax.hpp - Capa de fons del playfield: estrelles 2D amb parallax
|
||||
// © 2026 JailDesigner
|
||||
//
|
||||
// 3 capes de profunditat. Cada capa té estrelles amb brillantor, mida i
|
||||
// factor parallax propis. Les més properes són més brillants i grans i es
|
||||
// mouen més ràpid quan el món es desplaça; les més llunyanes són tènues i
|
||||
// petites i amb prou feines es mouen.
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace Defaults::StarfieldParallax {
|
||||
|
||||
namespace Far {
|
||||
constexpr int COUNT = 60;
|
||||
constexpr float BRIGHTNESS = 0.15F;
|
||||
constexpr float PARALLAX_FACTOR = 0.15F; // multiplicador sobre world_velocity
|
||||
constexpr int SIZE_PX = 1; // 1 px (punt)
|
||||
} // namespace Far
|
||||
|
||||
namespace Mid {
|
||||
constexpr int COUNT = 50;
|
||||
constexpr float BRIGHTNESS = 0.30F;
|
||||
constexpr float PARALLAX_FACTOR = 0.35F;
|
||||
constexpr int SIZE_PX = 2; // creu de 3x3 (extensió ±1)
|
||||
} // namespace Mid
|
||||
|
||||
namespace Near {
|
||||
constexpr int COUNT = 40;
|
||||
constexpr float BRIGHTNESS = 0.55F;
|
||||
constexpr float PARALLAX_FACTOR = 0.70F;
|
||||
constexpr int SIZE_PX = 3; // creu de 5x5 (extensió ±2)
|
||||
} // namespace Near
|
||||
|
||||
constexpr int TOTAL_COUNT = Far::COUNT + Mid::COUNT + Near::COUNT;
|
||||
|
||||
} // namespace Defaults::StarfieldParallax
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include "core/defaults/game.hpp"
|
||||
@@ -66,7 +68,7 @@ namespace Defaults::Title {
|
||||
constexpr float FLOATING_SCALE = 1.0F * SHIP_BASE_SCALE; // Flotante: scale base
|
||||
|
||||
// Offset de entrada (ajustat automáticoament a l'scale)
|
||||
// Fórmula: (radi màxim de la ship * scale de entrada) + margen
|
||||
// Fórmula: (radius màxim de la ship * scale de entrada) + margen
|
||||
constexpr float ENTRY_OFFSET = (SHIP_MAX_RADIUS * ENTRY_SCALE_START) + ENTRY_OFFSET_MARGIN;
|
||||
|
||||
// Vec2 de fuga (centro para l'animación de salida)
|
||||
@@ -79,7 +81,7 @@ namespace Defaults::Title {
|
||||
|
||||
// Durades de animación
|
||||
constexpr float ENTRY_DURATION = 2.0F; // Entrada (segons)
|
||||
constexpr float EXIT_DURATION = 1.0F; // Salida (segons)
|
||||
constexpr float EXIT_DURATION = 1.5F; // Salida (segons)
|
||||
|
||||
// Flotació (oscil·lació reduïda y diferenciada per ship)
|
||||
constexpr float FLOAT_AMPLITUDE_X = 4.0F; // Amplitud X (píxels)
|
||||
@@ -94,9 +96,6 @@ namespace Defaults::Title {
|
||||
constexpr float P1_ENTRY_DELAY = 0.0F; // P1 entra immediatament
|
||||
constexpr float P2_ENTRY_DELAY = 0.5F; // P2 entra 0.5s después
|
||||
|
||||
// Delay global antes de start l'animación de entrada al state MAIN
|
||||
constexpr float ENTRANCE_DELAY = 5.0F; // Temps de espera antes que las naves entrin
|
||||
|
||||
// Multiplicadors de freqüència para cada ship (variació sutil ±12%)
|
||||
constexpr float P1_FREQUENCY_MULTIPLIER = 0.88F; // 12% més lenta
|
||||
constexpr float P2_FREQUENCY_MULTIPLIER = 1.12F; // 12% més ràpida
|
||||
@@ -106,7 +105,7 @@ namespace Defaults::Title {
|
||||
namespace Layout {
|
||||
// Posicions verticals (anclatges des del TOP de pantalla lógica, 0.0-1.0)
|
||||
constexpr float LOGO_POS = 0.20F; // Logo "ORNI"
|
||||
constexpr float PRESS_START_POS = 0.75F; // "PRESS START TO PLAY"
|
||||
constexpr float PRESS_START_POS = 0.72F; // "PRESS START TO PLAY" (una mica més amunt)
|
||||
constexpr float COPYRIGHT1_POS = 0.90F; // Primera línia copyright
|
||||
|
||||
// Separacions relatives (proporció respecte Game::HEIGHT = 480px)
|
||||
@@ -117,6 +116,13 @@ namespace Defaults::Title {
|
||||
constexpr float LOGO_SCALE = 0.6F; // Escala "ORNI ATTACK!"
|
||||
constexpr float PRESS_START_SCALE = 1.0F; // Escala "PRESS START TO PLAY"
|
||||
constexpr float COPYRIGHT_SCALE = 0.5F; // Escala copyright
|
||||
constexpr float JAILGAMES_BRIGHTNESS = 0.8F; // Logo JAILGAMES una mica menys brillant
|
||||
constexpr float COPYRIGHT_BRIGHTNESS = 0.55F; // Mateix cian que JAILGAMES, però menys brillant
|
||||
|
||||
// Parpelleig del "PRESS START" (blinks per segon). Ritme pausat quan el
|
||||
// text apareix (MAIN) i més ràpid quan ja s'ha premut START (join phase).
|
||||
constexpr float PRESS_START_BLINK_HZ_SLOW = 1.0F;
|
||||
constexpr float PRESS_START_BLINK_HZ_FAST = 3.0F;
|
||||
constexpr float JAILGAMES_SCALE = 0.25F; // Escala del logo JAILGAMES pequeño sobre el copyright
|
||||
|
||||
// Separación entre el logo JAILGAMES y la línea de copyright (proporción de Game::HEIGHT).
|
||||
@@ -126,4 +132,50 @@ namespace Defaults::Title {
|
||||
constexpr float TEXT_SPACING = 2.0F;
|
||||
} // namespace Layout
|
||||
|
||||
// Coreografia de la seqüència d'entrada al state MAIN.
|
||||
// Tots els elements (logo, footer, naus, press start) entren ordenadament
|
||||
// segons aquests thresholds. Vegeu title_scene.cpp/updateMainState.
|
||||
//
|
||||
// Per al logo i el footer, l'efecte simula un moviment 3D des de l'usuari
|
||||
// cap al VP: el text arrenca gran i a la posició projectada extrema (com
|
||||
// si estigués prop de la càmera, fora de pantalla) i acaba a la seva
|
||||
// posició final amb escala normal (com si hagués aterrat al VP). Pivot:
|
||||
// centre de pantalla (= projecció del VP 3D).
|
||||
namespace Sequence {
|
||||
// Factor d'escala inicial. >1 = sprite gran a l'inici (prop de l'usuari).
|
||||
// La posició inicial es deriva: pivot=centre, delta multiplicat per aquest factor.
|
||||
constexpr float LOGO_INTRO_SCALE_START = 2.5F;
|
||||
constexpr float FOOTER_INTRO_SCALE_START = 2.5F;
|
||||
|
||||
// Durades de les animacions d'entrada (segons).
|
||||
constexpr float LOGO_ENTRY_DURATION = 1.2F;
|
||||
constexpr float JAILGAMES_ENTRY_DURATION = 0.7F;
|
||||
constexpr float COPYRIGHT_ENTRY_DURATION = 0.7F;
|
||||
|
||||
// Stagger "pam-pam" entre l'arrencada de JAILGAMES i la de COPYRIGHT.
|
||||
constexpr float COPYRIGHT_STAGGER = 0.18F;
|
||||
|
||||
// Delays entre etapes.
|
||||
constexpr float SHIPS_DELAY_AFTER_FOOTER = 0.20F;
|
||||
constexpr float PRESS_START_DELAY_AFTER_SHIPS = 0.40F;
|
||||
} // namespace Sequence
|
||||
|
||||
// Paleta neon de l'escena de títol (cian + magenta synthwave).
|
||||
// alpha = 255 (sentinela "color vàlid") fa que el pipeline ignori
|
||||
// el color global de l'oscil·lador per a aquesta crida.
|
||||
namespace Colors {
|
||||
// Ambre neon: el mateix to dels missatges d'inici/fi de fase
|
||||
// (STAGE_MESSAGE_COLOR a game_scene.cpp) per unificar el feel.
|
||||
constexpr SDL_Color AMBER = {.r = 255, .g = 200, .b = 70, .a = 255};
|
||||
|
||||
constexpr SDL_Color STARFIELD = {.r = 200, .g = 220, .b = 255, .a = 255}; // Blanc-blau gel
|
||||
constexpr SDL_Color LOGO_MAIN = {.r = 0, .g = 255, .b = 255, .a = 255}; // Cian pur
|
||||
constexpr SDL_Color LOGO_SHADOW = STARFIELD; // Color de l'starfield (offset)
|
||||
constexpr SDL_Color SHIP_P1 = {.r = 255, .g = 255, .b = 255, .a = 255}; // Blanc
|
||||
constexpr SDL_Color SHIP_P2 = {.r = 255, .g = 255, .b = 255, .a = 255}; // Blanc
|
||||
constexpr SDL_Color PRESS_START = AMBER; // Ambre (com les frases de fase)
|
||||
constexpr SDL_Color JAILGAMES_LOGO = {.r = 0, .g = 255, .b = 255, .a = 255}; // Cian pur
|
||||
constexpr SDL_Color COPYRIGHT = {.r = 0, .g = 255, .b = 255, .a = 255}; // Mateix cian (el brillo es baixa al render: COPYRIGHT_BRIGHTNESS)
|
||||
} // namespace Colors
|
||||
|
||||
} // namespace Defaults::Title
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace Defaults::Trail {
|
||||
|
||||
constexpr int POOL_SIZE = 200;
|
||||
|
||||
constexpr float SPEED_THRESHOLD_PX_S = 54.0F; // 30% de Physics::MAX_VELOCITY (180)
|
||||
constexpr float SPEED_THRESHOLD_PX_S = 54.0F; // 30% de player.yaml::physics.max_velocity (180 px/s)
|
||||
constexpr float EMIT_INTERVAL_S = 0.04F; // ~25 Hz nominal
|
||||
constexpr float EMIT_JITTER_S = 0.015F; // ±15 ms al cooldown
|
||||
constexpr float POSITION_JITTER_PX = 2.5F; // jitter al punt de naixement
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
// entity_loader.cpp - Implementació del carregador d'entitats YAML
|
||||
// © 2026 JailDesigner
|
||||
|
||||
#include "core/entities/entity_loader.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "core/resources/resource_helper.hpp"
|
||||
|
||||
namespace Entities {
|
||||
|
||||
std::unordered_map<std::string, std::shared_ptr<fkyaml::node>> EntityLoader::cache;
|
||||
|
||||
auto EntityLoader::load(const std::string& name) -> std::shared_ptr<fkyaml::node> {
|
||||
// Cache hit
|
||||
auto it = cache.find(name);
|
||||
if (it != cache.end()) {
|
||||
std::cout << "[EntityLoader] Cache hit: " << name << '\n';
|
||||
return it->second;
|
||||
}
|
||||
|
||||
const std::string PATH = "entities/" + name + "/" + name + ".yaml";
|
||||
|
||||
std::vector<uint8_t> data = Resource::Helper::loadFile(PATH);
|
||||
if (data.empty()) {
|
||||
std::cerr << "[EntityLoader] Error: no s'ha pogut load " << PATH << '\n';
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
try {
|
||||
std::string yaml_content(data.begin(), data.end());
|
||||
std::stringstream stream(yaml_content);
|
||||
auto node = std::make_shared<fkyaml::node>(fkyaml::node::deserialize(stream));
|
||||
|
||||
std::cout << "[EntityLoader] Carregat: " << PATH << '\n';
|
||||
cache[name] = node;
|
||||
return node;
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "[EntityLoader] Excepció parsejant " << PATH << ": " << e.what() << '\n';
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void EntityLoader::clearCache() {
|
||||
std::cout << "[EntityLoader] Netejant caché (" << cache.size() << " entitats)" << '\n';
|
||||
cache.clear();
|
||||
}
|
||||
|
||||
auto EntityLoader::getCacheSize() -> size_t { return cache.size(); }
|
||||
|
||||
} // namespace Entities
|
||||
@@ -0,0 +1,38 @@
|
||||
// entity_loader.hpp - Carregador genèric de descriptors d'entitats en YAML
|
||||
// © 2026 JailDesigner
|
||||
//
|
||||
// Cada entitat viu a `data/entities/<name>/<name>.yaml` (mateix patró que el
|
||||
// projecte germà aee_arcade). Aquest loader resol el path, llegeix del
|
||||
// resource pack via Resource::Helper, parseja amb fkyaml i cacheja el node
|
||||
// per evitar relectures. Retorna nullptr en cas d'error (el caller decideix
|
||||
// si abortar).
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "external/fkyaml_node.hpp"
|
||||
|
||||
namespace Entities {
|
||||
|
||||
class EntityLoader {
|
||||
public:
|
||||
EntityLoader() = delete; // tot estàtic
|
||||
|
||||
// Carrega el descriptor d'una entitat per nom (ex. "player" →
|
||||
// "entities/player/player.yaml"). Retorna nullptr si no es pot
|
||||
// carregar o parsejar. Cachejat per nom.
|
||||
static auto load(const std::string& name) -> std::shared_ptr<fkyaml::node>;
|
||||
|
||||
// Buidar caché (útil per debug/recàrrega).
|
||||
static void clearCache();
|
||||
|
||||
[[nodiscard]] static auto getCacheSize() -> size_t;
|
||||
|
||||
private:
|
||||
static std::unordered_map<std::string, std::shared_ptr<fkyaml::node>> cache;
|
||||
};
|
||||
|
||||
} // namespace Entities
|
||||
@@ -23,12 +23,12 @@ namespace Graphics {
|
||||
}
|
||||
|
||||
void Border::bumpAt(Vec2 contact_point, float strength) {
|
||||
const SDL_FRect& zona = Defaults::Zones::PLAYAREA;
|
||||
const SDL_FRect& zone = Defaults::Zones::PLAYAREA;
|
||||
const std::array<float, SIDE_COUNT> DISTANCES = {
|
||||
/* TOP */ std::abs(contact_point.y - zona.y),
|
||||
/* RIGHT */ std::abs((zona.x + zona.w) - contact_point.x),
|
||||
/* BOTTOM */ std::abs((zona.y + zona.h) - contact_point.y),
|
||||
/* LEFT */ std::abs(contact_point.x - zona.x)};
|
||||
/* TOP */ std::abs(contact_point.y - zone.y),
|
||||
/* RIGHT */ std::abs((zone.x + zone.w) - contact_point.x),
|
||||
/* BOTTOM */ std::abs((zone.y + zone.h) - contact_point.y),
|
||||
/* LEFT */ std::abs(contact_point.x - zone.x)};
|
||||
|
||||
int closest_idx = 0;
|
||||
float closest_dist = DISTANCES[0];
|
||||
@@ -51,12 +51,12 @@ namespace Graphics {
|
||||
|
||||
namespace {
|
||||
|
||||
// Lerp de l'oscil·lador (color base actual) cap a un color "flash" en
|
||||
// funció de f ∈ [0, 1]. Retorna sempre amb alpha>0 perquè el line_renderer
|
||||
// l'use directament (sense barrejar amb el global).
|
||||
// Lerp del color base verd fòsfor cap a un color "flash" en funció de
|
||||
// f ∈ [0, 1]. Retorna sempre amb alpha>0 perquè el line_renderer l'usi
|
||||
// directament (sense caure al fallback DEFAULT_LINE_COLOR).
|
||||
auto lerpColor(SDL_Color flash, float f) -> SDL_Color {
|
||||
const float CLAMPED = std::clamp(f, 0.0F, 1.0F);
|
||||
const SDL_Color BASE = Rendering::getLineColor();
|
||||
constexpr SDL_Color BASE = Rendering::DEFAULT_LINE_COLOR;
|
||||
const auto LERP_U8 = [&](unsigned char a, unsigned char b) {
|
||||
const float OUT = (static_cast<float>(a) * (1.0F - CLAMPED)) + (static_cast<float>(b) * CLAMPED);
|
||||
return static_cast<unsigned char>(OUT);
|
||||
@@ -71,11 +71,11 @@ namespace Graphics {
|
||||
} // namespace
|
||||
|
||||
void Border::draw() const {
|
||||
const SDL_FRect& zona = Defaults::Zones::PLAYAREA;
|
||||
const int X1 = static_cast<int>(zona.x);
|
||||
const int Y1 = static_cast<int>(zona.y);
|
||||
const int X2 = static_cast<int>(zona.x + zona.w);
|
||||
const int Y2 = static_cast<int>(zona.y + zona.h);
|
||||
const SDL_FRect& zone = Defaults::Zones::PLAYAREA;
|
||||
const int X1 = static_cast<int>(zone.x);
|
||||
const int Y1 = static_cast<int>(zone.y);
|
||||
const int X2 = static_cast<int>(zone.x + zone.w);
|
||||
const int Y2 = static_cast<int>(zone.y + zone.h);
|
||||
|
||||
const int OFF_TOP = static_cast<int>(sides_[SIDE_TOP].displacement_px);
|
||||
const int OFF_RIGHT = static_cast<int>(sides_[SIDE_RIGHT].displacement_px);
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
// curtain.cpp - Implementació de la cortinilla negra
|
||||
// © 2026 JailDesigner
|
||||
|
||||
#include "core/graphics/curtain.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "core/defaults/game.hpp"
|
||||
#include "core/math/easing.hpp"
|
||||
|
||||
namespace Graphics {
|
||||
|
||||
namespace {
|
||||
constexpr float SCREEN_H = static_cast<float>(Defaults::Game::HEIGHT);
|
||||
constexpr float SCREEN_W = static_cast<float>(Defaults::Game::WIDTH);
|
||||
} // namespace
|
||||
|
||||
Curtain::Curtain(Rendering::Renderer* renderer)
|
||||
: renderer_(renderer) {}
|
||||
|
||||
void Curtain::cover(float duration) {
|
||||
// Caire superior de -H (fora, a dalt) fins a 0 (tela tapant tota la pantalla).
|
||||
from_ = -SCREEN_H;
|
||||
to_ = 0.0F;
|
||||
duration_ = duration;
|
||||
elapsed_ = 0.0F;
|
||||
active_ = true;
|
||||
}
|
||||
|
||||
void Curtain::reveal(float duration) {
|
||||
// Caire superior de 0 (tapant) fins a +H (tela fora per baix).
|
||||
from_ = 0.0F;
|
||||
to_ = SCREEN_H;
|
||||
duration_ = duration;
|
||||
elapsed_ = 0.0F;
|
||||
active_ = true;
|
||||
}
|
||||
|
||||
void Curtain::update(float delta_time) {
|
||||
if (!active_) {
|
||||
return;
|
||||
}
|
||||
elapsed_ += delta_time;
|
||||
}
|
||||
|
||||
auto Curtain::topY() const -> float {
|
||||
if (duration_ <= 0.0F) {
|
||||
return to_;
|
||||
}
|
||||
const float T = std::clamp(elapsed_ / duration_, 0.0F, 1.0F);
|
||||
// Ease-in: la tela "cau" accelerant, com per gravetat.
|
||||
return Easing::lerp(from_, to_, Easing::easeInQuad(T));
|
||||
}
|
||||
|
||||
auto Curtain::isDone() const -> bool {
|
||||
return !active_ || elapsed_ >= duration_;
|
||||
}
|
||||
|
||||
void Curtain::draw() const {
|
||||
if (!active_) {
|
||||
return;
|
||||
}
|
||||
const float TOP = topY();
|
||||
// Si la tela ja ha sortit completament per baix, no hi ha res a pintar.
|
||||
if (TOP >= SCREEN_H) {
|
||||
return;
|
||||
}
|
||||
renderer_->pushRect(0.0F, TOP, SCREEN_W, SCREEN_H, 0.0F, 0.0F, 0.0F, 1.0F);
|
||||
}
|
||||
|
||||
} // namespace Graphics
|
||||
@@ -0,0 +1,48 @@
|
||||
// curtain.hpp - Cortinilla negra per a transicions d'escena
|
||||
// © 2026 JailDesigner
|
||||
//
|
||||
// Tela negra a pantalla completa que es mou SEMPRE cap avall:
|
||||
// - cover(): cau des de dalt fins a tapar-ho tot (queda negre).
|
||||
// - reveal(): segueix caient i surt per baix, deixant veure l'escena.
|
||||
// Una escena la posseeix, l'actualitza cada frame i la dibuixa l'ÚLTIM (per
|
||||
// damunt de tot). En repòs (no arrencada o reveal acabada) el draw() és no-op.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "core/rendering/render_context.hpp"
|
||||
|
||||
namespace Graphics {
|
||||
|
||||
class Curtain {
|
||||
public:
|
||||
explicit Curtain(Rendering::Renderer* renderer);
|
||||
|
||||
// Tela que cau des de dalt fins a tapar tota la pantalla en 'duration' s.
|
||||
void cover(float duration);
|
||||
|
||||
// Tela que segueix caient i surt per baix (destapa) en 'duration' s.
|
||||
void reveal(float duration);
|
||||
|
||||
// Avança el temporitzador intern.
|
||||
void update(float delta_time);
|
||||
|
||||
// Dibuixa la tela negra a la seva posició vertical actual. No-op si no
|
||||
// queda res visible.
|
||||
void draw() const;
|
||||
|
||||
// Cert quan el moviment actual ha acabat (o no s'ha arrencat mai).
|
||||
[[nodiscard]] auto isDone() const -> bool;
|
||||
|
||||
private:
|
||||
// Posició actual del caire superior de la tela (píxels lògics).
|
||||
[[nodiscard]] auto topY() const -> float;
|
||||
|
||||
Rendering::Renderer* renderer_;
|
||||
float from_{0.0F};
|
||||
float to_{0.0F};
|
||||
float duration_{0.0F};
|
||||
float elapsed_{0.0F};
|
||||
bool active_{false};
|
||||
};
|
||||
|
||||
} // namespace Graphics
|
||||
+213
-168
@@ -5,8 +5,8 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <limits>
|
||||
|
||||
#include "core/defaults.hpp"
|
||||
#include "core/rendering/line_renderer.hpp"
|
||||
@@ -21,20 +21,38 @@ namespace Graphics {
|
||||
return 1.0F - (INV * INV * INV);
|
||||
}
|
||||
|
||||
// Lerp del color base actual (oscil·lador) cap a un color destí en
|
||||
// funció de f ∈ [0, 1]. Alpha > 0 perquè line_renderer l'usi directe.
|
||||
auto lerpColor(SDL_Color target, float f) -> SDL_Color {
|
||||
const float CLAMPED = std::clamp(f, 0.0F, 1.0F);
|
||||
const SDL_Color BASE = Rendering::getLineColor();
|
||||
const auto LERP_U8 = [&](unsigned char a, unsigned char b) {
|
||||
const float OUT = (static_cast<float>(a) * (1.0F - CLAMPED)) + (static_cast<float>(b) * CLAMPED);
|
||||
return static_cast<unsigned char>(OUT);
|
||||
};
|
||||
return SDL_Color{
|
||||
.r = LERP_U8(BASE.r, target.r),
|
||||
.g = LERP_U8(BASE.g, target.g),
|
||||
.b = LERP_U8(BASE.b, target.b),
|
||||
.a = 255};
|
||||
auto randUniform(float min_v, float max_v) -> float {
|
||||
const float NORM = static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX);
|
||||
return min_v + (NORM * (max_v - min_v));
|
||||
}
|
||||
|
||||
// Desplaçament radial acumulat al punt (px, py) sumant totes les ripples
|
||||
// que el toquen. Retorna {dx, dy} a sumar a la posició original.
|
||||
auto computeRippleDisplacement(float px, float py, const Playfield::Ripple* const* hits, int n_hits) -> Vec2 {
|
||||
float dx_total = 0.0F;
|
||||
float dy_total = 0.0F;
|
||||
for (int i = 0; i < n_hits; i++) {
|
||||
const auto& r = *hits[i];
|
||||
const float RADIUS = r.age_s * r.speed_px_s;
|
||||
const float THICKNESS = r.thickness_px;
|
||||
const float DX = px - r.center.x;
|
||||
const float DY = py - r.center.y;
|
||||
const float D = std::sqrt((DX * DX) + (DY * DY));
|
||||
if (D < 0.001F) {
|
||||
continue; // centre exacte: no hi ha direcció radial
|
||||
}
|
||||
const float PHASE = (D - RADIUS) / THICKNESS;
|
||||
if (std::fabs(PHASE) >= 1.0F) {
|
||||
continue; // fora de l'anell d'aquesta ripple
|
||||
}
|
||||
const float ENVELOPE = std::cos(PHASE * Defaults::Math::PI * 0.5F);
|
||||
const float AMP_EFF = r.amplitude_px * (1.0F - (r.age_s / r.lifetime_s));
|
||||
const float UX = DX / D;
|
||||
const float UY = DY / D;
|
||||
dx_total += UX * AMP_EFF * ENVELOPE;
|
||||
dy_total += UY * AMP_EFF * ENVELOPE;
|
||||
}
|
||||
return Vec2{.x = dx_total, .y = dy_total};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -44,103 +62,94 @@ namespace Graphics {
|
||||
buildLines();
|
||||
}
|
||||
|
||||
void Playfield::completeBuild() {
|
||||
// Avançar el rellotge intern més enllà de tota la finestra d'spawn + el
|
||||
// creixement de l'última línia: computeLineProgress() retorna 1.0 per a totes.
|
||||
elapsed_s_ = Defaults::Playfield::TOTAL_ANIMATION_DURATION_S;
|
||||
}
|
||||
|
||||
void Playfield::update(float delta_time) {
|
||||
elapsed_s_ += delta_time;
|
||||
|
||||
// Decau l'orbit i avança la fase del sin per cada línia.
|
||||
const float ORBIT_DELTA_PHASE = Defaults::Playfield::ORBIT_FREQ_HZ * 2.0F * Defaults::Math::PI * delta_time;
|
||||
const float ORBIT_DEC = Defaults::Playfield::ORBIT_DECAY_PER_S * delta_time;
|
||||
for (auto& line : lines_) {
|
||||
line.orbit_phase += ORBIT_DELTA_PHASE;
|
||||
line.orbit_amplitude = std::max(0.0F, line.orbit_amplitude - ORBIT_DEC);
|
||||
|
||||
// Avança els pulses; els desactiva quan acaben de vida.
|
||||
for (auto& pulse : line.pulses) {
|
||||
if (!pulse.active) {
|
||||
for (auto& ripple : ripples_) {
|
||||
if (!ripple.active) {
|
||||
continue;
|
||||
}
|
||||
pulse.age_s += delta_time;
|
||||
if (pulse.age_s >= Defaults::Playfield::PULSE_LIFETIME_S) {
|
||||
pulse.active = false;
|
||||
}
|
||||
ripple.age_s += delta_time;
|
||||
if (ripple.age_s >= ripple.lifetime_s) {
|
||||
ripple.active = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Playfield::spawnPulseAt(Line& line, float center_t) {
|
||||
for (auto& pulse : line.pulses) {
|
||||
if (!pulse.active) {
|
||||
pulse.active = true;
|
||||
pulse.age_s = 0.0F;
|
||||
pulse.center_t = std::clamp(center_t, 0.0F, 1.0F);
|
||||
auto Playfield::findFreeRipple() -> Ripple* {
|
||||
Ripple* oldest = nullptr;
|
||||
for (auto& ripple : ripples_) {
|
||||
if (!ripple.active) {
|
||||
return &ripple;
|
||||
}
|
||||
if (oldest == nullptr || ripple.age_s > oldest->age_s) {
|
||||
oldest = &ripple;
|
||||
}
|
||||
}
|
||||
return oldest; // pool ple: substituïm la més vella
|
||||
}
|
||||
|
||||
void Playfield::spawnBig(Vec2 pos) {
|
||||
Ripple* r = findFreeRipple();
|
||||
if (r == nullptr) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Cap slot lliure: substituïm el més vell.
|
||||
Pulse* oldest = line.pulses.data();
|
||||
for (auto& pulse : line.pulses) {
|
||||
if (pulse.age_s > oldest->age_s) {
|
||||
oldest = &pulse;
|
||||
}
|
||||
}
|
||||
oldest->active = true;
|
||||
oldest->age_s = 0.0F;
|
||||
oldest->center_t = std::clamp(center_t, 0.0F, 1.0F);
|
||||
r->center = pos;
|
||||
r->age_s = 0.0F;
|
||||
r->lifetime_s = Defaults::Playfield::Ripple::BIG_LIFETIME_S;
|
||||
r->speed_px_s = Defaults::Playfield::Ripple::BIG_SPEED_PX_S;
|
||||
r->amplitude_px = Defaults::Playfield::Ripple::BIG_AMPLITUDE_PX;
|
||||
r->thickness_px = Defaults::Playfield::Ripple::BIG_THICKNESS_PX;
|
||||
r->active = true;
|
||||
}
|
||||
|
||||
void Playfield::notifyFireworkSpawn(Vec2 pos) {
|
||||
// Línia vertical més propera (per posició x) i horitzontal més propera (per y).
|
||||
Line* closest_v = nullptr;
|
||||
Line* closest_h = nullptr;
|
||||
float min_dx = std::numeric_limits<float>::max();
|
||||
float min_dy = std::numeric_limits<float>::max();
|
||||
for (auto& line : lines_) {
|
||||
if (line.is_vertical) {
|
||||
const float DX = std::abs(pos.x - line.start.x);
|
||||
if (DX < min_dx) {
|
||||
min_dx = DX;
|
||||
closest_v = &line;
|
||||
}
|
||||
} else {
|
||||
const float DY = std::abs(pos.y - line.start.y);
|
||||
if (DY < min_dy) {
|
||||
min_dy = DY;
|
||||
closest_h = &line;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (closest_v != nullptr) {
|
||||
const float LINE_LEN = closest_v->end.y - closest_v->start.y;
|
||||
const float CENTER_T = (LINE_LEN > 0.0F) ? (pos.y - closest_v->start.y) / LINE_LEN : 0.5F;
|
||||
spawnPulseAt(*closest_v, CENTER_T);
|
||||
}
|
||||
if (closest_h != nullptr) {
|
||||
const float LINE_LEN = closest_h->end.x - closest_h->start.x;
|
||||
const float CENTER_T = (LINE_LEN > 0.0F) ? (pos.x - closest_h->start.x) / LINE_LEN : 0.5F;
|
||||
spawnPulseAt(*closest_h, CENTER_T);
|
||||
}
|
||||
}
|
||||
|
||||
void Playfield::notifyShipPass(Vec2 pos, float speed_px_s) {
|
||||
if (speed_px_s < Defaults::Playfield::ORBIT_SHIP_SPEED_THRESHOLD) {
|
||||
void Playfield::spawnSmall(Vec2 pos) {
|
||||
Ripple* r = findFreeRipple();
|
||||
if (r == nullptr) {
|
||||
return;
|
||||
}
|
||||
const float MAX_DIST = Defaults::Playfield::ORBIT_PROXIMITY_PX;
|
||||
for (auto& line : lines_) {
|
||||
// Distància perpendicular del punt a la línia (que és horitzontal o vertical).
|
||||
const float DIST = line.is_vertical
|
||||
? std::abs(pos.x - line.start.x)
|
||||
: std::abs(pos.y - line.start.y);
|
||||
if (DIST < MAX_DIST) {
|
||||
line.orbit_amplitude = Defaults::Playfield::ORBIT_AMPLITUDE_MAX_PX;
|
||||
r->center = pos;
|
||||
r->age_s = 0.0F;
|
||||
r->lifetime_s = Defaults::Playfield::Ripple::SMALL_LIFETIME_S;
|
||||
r->speed_px_s = Defaults::Playfield::Ripple::SMALL_SPEED_PX_S;
|
||||
r->amplitude_px = Defaults::Playfield::Ripple::SMALL_AMPLITUDE_PX;
|
||||
r->thickness_px = Defaults::Playfield::Ripple::SMALL_THICKNESS_PX;
|
||||
r->active = true;
|
||||
}
|
||||
|
||||
void Playfield::notifyExplosion(Vec2 pos) {
|
||||
spawnBig(pos);
|
||||
}
|
||||
|
||||
void Playfield::notifyShipMoving(std::uint8_t player_id, Vec2 pos, float speed_px_s, float delta_time) {
|
||||
if (player_id >= ship_ripple_cooldown_.size()) {
|
||||
return;
|
||||
}
|
||||
if (speed_px_s < Defaults::Playfield::Ripple::SHIP_SPEED_THRESHOLD_PX_S) {
|
||||
ship_ripple_cooldown_[player_id] = 0.0F;
|
||||
return;
|
||||
}
|
||||
ship_ripple_cooldown_[player_id] -= delta_time;
|
||||
if (ship_ripple_cooldown_[player_id] > 0.0F) {
|
||||
return;
|
||||
}
|
||||
spawnSmall(pos);
|
||||
const float JITTER = randUniform(
|
||||
-Defaults::Playfield::Ripple::SHIP_COOLDOWN_JITTER_S,
|
||||
Defaults::Playfield::Ripple::SHIP_COOLDOWN_JITTER_S);
|
||||
ship_ripple_cooldown_[player_id] =
|
||||
Defaults::Playfield::Ripple::SHIP_COOLDOWN_S + JITTER;
|
||||
}
|
||||
|
||||
void Playfield::buildLines() {
|
||||
const SDL_FRect& zona = Defaults::Zones::PLAYAREA;
|
||||
const float CELL_W = zona.w / static_cast<float>(Defaults::Playfield::COLUMNS);
|
||||
const float CELL_H = zona.h / static_cast<float>(Defaults::Playfield::ROWS);
|
||||
const SDL_FRect& zone = Defaults::Zones::PLAYAREA;
|
||||
const float CELL_W = zone.w / static_cast<float>(Defaults::Playfield::COLUMNS);
|
||||
const float CELL_H = zone.h / static_cast<float>(Defaults::Playfield::ROWS);
|
||||
const float SUB_W = CELL_W / static_cast<float>(Defaults::Playfield::SUBDIVISIONS);
|
||||
const float SUB_H = CELL_H / static_cast<float>(Defaults::Playfield::SUBDIVISIONS);
|
||||
const int SUB_VERTS = Defaults::Playfield::COLUMNS * Defaults::Playfield::SUBDIVISIONS;
|
||||
@@ -151,38 +160,32 @@ namespace Graphics {
|
||||
|
||||
// Verticals: posicions i ∈ [1, SUB_VERTS-1].
|
||||
for (int i = 1; i < SUB_VERTS; i++) {
|
||||
const float X = zona.x + (static_cast<float>(i) * SUB_W);
|
||||
const float X = zone.x + (static_cast<float>(i) * SUB_W);
|
||||
const bool IS_MAIN = (i % Defaults::Playfield::SUBDIVISIONS) == 0;
|
||||
const float BRIGHTNESS = IS_MAIN
|
||||
? Defaults::Playfield::GRID_BRIGHTNESS
|
||||
: Defaults::Playfield::SUBGRID_BRIGHTNESS;
|
||||
verticals.push_back(Line{
|
||||
.start = {.x = X, .y = zona.y},
|
||||
.end = {.x = X, .y = zona.y + zona.h},
|
||||
.start = {.x = X, .y = zone.y},
|
||||
.end = {.x = X, .y = zone.y + zone.h},
|
||||
.brightness = BRIGHTNESS,
|
||||
.spawn_time_s = 0.0F,
|
||||
.is_vertical = true,
|
||||
.orbit_amplitude = 0.0F,
|
||||
.orbit_phase = 0.0F,
|
||||
.pulses = {}});
|
||||
.is_vertical = true});
|
||||
}
|
||||
|
||||
// Horitzontals: posicions j ∈ [1, SUB_HORIZ-1].
|
||||
for (int j = 1; j < SUB_HORIZ; j++) {
|
||||
const float Y = zona.y + (static_cast<float>(j) * SUB_H);
|
||||
const float Y = zone.y + (static_cast<float>(j) * SUB_H);
|
||||
const bool IS_MAIN = (j % Defaults::Playfield::SUBDIVISIONS) == 0;
|
||||
const float BRIGHTNESS = IS_MAIN
|
||||
? Defaults::Playfield::GRID_BRIGHTNESS
|
||||
: Defaults::Playfield::SUBGRID_BRIGHTNESS;
|
||||
horizontals.push_back(Line{
|
||||
.start = {.x = zona.x, .y = Y},
|
||||
.end = {.x = zona.x + zona.w, .y = Y},
|
||||
.start = {.x = zone.x, .y = Y},
|
||||
.end = {.x = zone.x + zone.w, .y = Y},
|
||||
.brightness = BRIGHTNESS,
|
||||
.spawn_time_s = 0.0F,
|
||||
.is_vertical = false,
|
||||
.orbit_amplitude = 0.0F,
|
||||
.orbit_phase = 0.0F,
|
||||
.pulses = {}});
|
||||
.is_vertical = false});
|
||||
}
|
||||
|
||||
// Ona diagonal: la línia esquerra/superior naix a t=0 i les següents
|
||||
@@ -199,13 +202,39 @@ namespace Graphics {
|
||||
lines_.clear();
|
||||
lines_.reserve(verticals.size() + horizontals.size());
|
||||
|
||||
// El spawn_time_s s'assigna per índex espacial perquè la diagonal de
|
||||
// l'ona de creixement avanci uniformement. L'ordre dins lines_, en
|
||||
// canvi, ha de garantir que el grid principal (més brillant) es
|
||||
// dibuixi DESPRÉS del subgrid: així a les interseccions guanya el
|
||||
// principal i no queden tallades pel subgrid.
|
||||
for (int i = 0; i < NUM_V; i++) {
|
||||
verticals[i].spawn_time_s = static_cast<float>(i) * INTERVAL_V;
|
||||
lines_.push_back(verticals[i]);
|
||||
}
|
||||
for (int i = 0; i < NUM_H; i++) {
|
||||
horizontals[i].spawn_time_s = static_cast<float>(i) * INTERVAL_H;
|
||||
lines_.push_back(horizontals[i]);
|
||||
}
|
||||
|
||||
// Passada 1: subgrid (verticals + horitzontals).
|
||||
for (const auto& v : verticals) {
|
||||
if (v.brightness < Defaults::Playfield::GRID_BRIGHTNESS) {
|
||||
lines_.push_back(v);
|
||||
}
|
||||
}
|
||||
for (const auto& h : horizontals) {
|
||||
if (h.brightness < Defaults::Playfield::GRID_BRIGHTNESS) {
|
||||
lines_.push_back(h);
|
||||
}
|
||||
}
|
||||
// Passada 2: grid principal (verticals + horitzontals).
|
||||
for (const auto& v : verticals) {
|
||||
if (v.brightness >= Defaults::Playfield::GRID_BRIGHTNESS) {
|
||||
lines_.push_back(v);
|
||||
}
|
||||
}
|
||||
for (const auto& h : horizontals) {
|
||||
if (h.brightness >= Defaults::Playfield::GRID_BRIGHTNESS) {
|
||||
lines_.push_back(h);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,90 +244,106 @@ namespace Graphics {
|
||||
}
|
||||
|
||||
void Playfield::draw() const {
|
||||
// Recollir ripples actives (punters per accés ràpid al hot loop).
|
||||
std::array<const Ripple*, Defaults::Playfield::Ripple::POOL_SIZE> active{};
|
||||
int n_active = 0;
|
||||
for (const auto& ripple : ripples_) {
|
||||
if (ripple.active) {
|
||||
active[n_active++] = &ripple;
|
||||
}
|
||||
}
|
||||
for (const auto& line : lines_) {
|
||||
drawLine(line, active.data(), n_active);
|
||||
}
|
||||
}
|
||||
|
||||
void Playfield::drawLine(const Line& line, const Ripple* const* active, int n_active) const {
|
||||
const float RAW_P = computeLineProgress(line);
|
||||
if (RAW_P <= 0.0F) {
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
const float P = easeOutCubic(RAW_P);
|
||||
|
||||
// Desplaçament perpendicular per orbit (verticals → x, horitzontals → y).
|
||||
const float ORBIT_OFFSET = line.orbit_amplitude * std::sin(line.orbit_phase);
|
||||
const float ORBIT_DX = line.is_vertical ? ORBIT_OFFSET : 0.0F;
|
||||
const float ORBIT_DY = line.is_vertical ? 0.0F : ORBIT_OFFSET;
|
||||
|
||||
const float START_X = line.start.x + ORBIT_DX;
|
||||
const float START_Y = line.start.y + ORBIT_DY;
|
||||
const float START_X = line.start.x;
|
||||
const float START_Y = line.start.y;
|
||||
const float DX = line.end.x - line.start.x;
|
||||
const float DY = line.end.y - line.start.y;
|
||||
const float CURRENT_X = START_X + (DX * P);
|
||||
const float CURRENT_Y = START_Y + (DY * P);
|
||||
const float END_X = START_X + (DX * P);
|
||||
const float END_Y = START_Y + (DY * P);
|
||||
|
||||
// Tram base (brillo de la línia).
|
||||
// AABB de la porció visible de la línia + filtre de ripples.
|
||||
const float LINE_MIN_X = std::min(START_X, END_X);
|
||||
const float LINE_MAX_X = std::max(START_X, END_X);
|
||||
const float LINE_MIN_Y = std::min(START_Y, END_Y);
|
||||
const float LINE_MAX_Y = std::max(START_Y, END_Y);
|
||||
std::array<const Ripple*, Defaults::Playfield::Ripple::POOL_SIZE> hits{};
|
||||
int n_hits = 0;
|
||||
for (int i = 0; i < n_active; i++) {
|
||||
const auto& r = *active[i];
|
||||
const float R_MAX = (r.age_s * r.speed_px_s) + r.thickness_px;
|
||||
if ((r.center.x + R_MAX) < LINE_MIN_X || (r.center.x - R_MAX) > LINE_MAX_X ||
|
||||
(r.center.y + R_MAX) < LINE_MIN_Y || (r.center.y - R_MAX) > LINE_MAX_Y) {
|
||||
continue;
|
||||
}
|
||||
hits[n_hits++] = &r;
|
||||
}
|
||||
|
||||
if (n_hits == 0) {
|
||||
// Camí ràpid: una sola crida com abans.
|
||||
Rendering::linea(
|
||||
renderer_,
|
||||
static_cast<int>(START_X),
|
||||
static_cast<int>(START_Y),
|
||||
static_cast<int>(CURRENT_X),
|
||||
static_cast<int>(CURRENT_Y),
|
||||
line.brightness);
|
||||
|
||||
// Cap brillant mentre creix: l'últim tram de la línia es repinta més brillant.
|
||||
static_cast<int>(END_X),
|
||||
static_cast<int>(END_Y),
|
||||
line.brightness,
|
||||
0.0F,
|
||||
Defaults::Playfield::GRID_COLOR);
|
||||
// Cap brillant mentre creix.
|
||||
if (P < 1.0F) {
|
||||
const float LENGTH = std::sqrt((DX * DX) + (DY * DY));
|
||||
if (LENGTH > 0.0F) {
|
||||
const float HEAD_T = std::max(0.0F, P - (Defaults::Playfield::HEAD_LENGTH_PX / LENGTH));
|
||||
const float HEAD_X = START_X + (DX * HEAD_T);
|
||||
const float HEAD_Y = START_Y + (DY * HEAD_T);
|
||||
Rendering::linea(
|
||||
renderer_,
|
||||
static_cast<int>(HEAD_X),
|
||||
static_cast<int>(HEAD_Y),
|
||||
static_cast<int>(CURRENT_X),
|
||||
static_cast<int>(CURRENT_Y),
|
||||
Defaults::Playfield::HEAD_BRIGHTNESS);
|
||||
static_cast<int>(START_X + (DX * HEAD_T)),
|
||||
static_cast<int>(START_Y + (DY * HEAD_T)),
|
||||
static_cast<int>(END_X),
|
||||
static_cast<int>(END_Y),
|
||||
Defaults::Playfield::HEAD_BRIGHTNESS,
|
||||
0.0F,
|
||||
Defaults::Playfield::GRID_COLOR);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Pulses: cada un és un segment brillant centrat a center_t que
|
||||
// s'expandeix amb el temps i s'apaga.
|
||||
const float LINE_LENGTH = std::sqrt((DX * DX) + (DY * DY));
|
||||
if (LINE_LENGTH <= 0.0F) {
|
||||
continue;
|
||||
}
|
||||
const SDL_Color PULSE_TARGET = {
|
||||
.r = Defaults::Playfield::PULSE_COLOR_R,
|
||||
.g = Defaults::Playfield::PULSE_COLOR_G,
|
||||
.b = Defaults::Playfield::PULSE_COLOR_B,
|
||||
.a = 255};
|
||||
for (const auto& pulse : line.pulses) {
|
||||
if (!pulse.active) {
|
||||
continue;
|
||||
}
|
||||
const float HALF_WIDTH_T = (pulse.age_s * Defaults::Playfield::PULSE_SPREAD_PER_S) / LINE_LENGTH;
|
||||
const float INTENSITY = std::max(
|
||||
0.0F,
|
||||
1.0F - (pulse.age_s / Defaults::Playfield::PULSE_LIFETIME_S));
|
||||
const float T1 = std::clamp(pulse.center_t - HALF_WIDTH_T, 0.0F, 1.0F);
|
||||
const float T2 = std::clamp(pulse.center_t + HALF_WIDTH_T, 0.0F, 1.0F);
|
||||
if (T2 <= T1) {
|
||||
continue;
|
||||
}
|
||||
const float P1_X = START_X + (DX * T1);
|
||||
const float P1_Y = START_Y + (DY * T1);
|
||||
const float P2_X = START_X + (DX * T2);
|
||||
const float P2_Y = START_Y + (DY * T2);
|
||||
const SDL_Color SEG_COLOR = lerpColor(PULSE_TARGET, INTENSITY);
|
||||
// Camí deformat: subdividir en N segments i desplaçar cada vèrtex.
|
||||
const bool IS_MAIN = line.brightness >= Defaults::Playfield::GRID_BRIGHTNESS;
|
||||
const int N = IS_MAIN
|
||||
? Defaults::Playfield::Ripple::MAIN_SEGMENTS
|
||||
: Defaults::Playfield::Ripple::SUB_SEGMENTS;
|
||||
const Vec2 D0 = computeRippleDisplacement(START_X, START_Y, hits.data(), n_hits);
|
||||
float prev_x = START_X + D0.x;
|
||||
float prev_y = START_Y + D0.y;
|
||||
for (int i = 1; i <= N; i++) {
|
||||
const float T = static_cast<float>(i) / static_cast<float>(N);
|
||||
const float X = START_X + (DX * P * T);
|
||||
const float Y = START_Y + (DY * P * T);
|
||||
const Vec2 D = computeRippleDisplacement(X, Y, hits.data(), n_hits);
|
||||
const float NX = X + D.x;
|
||||
const float NY = Y + D.y;
|
||||
Rendering::linea(
|
||||
renderer_,
|
||||
static_cast<int>(P1_X),
|
||||
static_cast<int>(P1_Y),
|
||||
static_cast<int>(P2_X),
|
||||
static_cast<int>(P2_Y),
|
||||
1.0F,
|
||||
static_cast<int>(prev_x),
|
||||
static_cast<int>(prev_y),
|
||||
static_cast<int>(NX),
|
||||
static_cast<int>(NY),
|
||||
line.brightness,
|
||||
0.0F,
|
||||
SEG_COLOR);
|
||||
}
|
||||
Defaults::Playfield::GRID_COLOR);
|
||||
prev_x = NX;
|
||||
prev_y = NY;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,13 +5,16 @@
|
||||
// rep un `creation_progress` global ∈ [0, 1] i cada línia computa quina porció
|
||||
// li toca dibuixar segons el seu slot a la timeline.
|
||||
//
|
||||
// Disseny preparat per a futures capacitats:
|
||||
// - Línies "vives" que reaccionen a explosions / pas de la nau (reaction_intensity).
|
||||
// - Capes addicionals al fons (estrelles, gradients, scanlines).
|
||||
// Reaccions disponibles:
|
||||
// - Ripples: deformacions circulars (ones d'aigua) que travessen la graella.
|
||||
// Disparades per explosions (grans) i pas de la nau (petites, cadència estil
|
||||
// trail). Cada vèrtex d'una línia afectada es desplaça radialment cap a fora
|
||||
// amb una envoltant en cos(·) que decau a les vores de l'anell i amb el temps.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include "core/defaults/playfield.hpp"
|
||||
@@ -24,44 +27,56 @@ namespace Graphics {
|
||||
public:
|
||||
explicit Playfield(Rendering::Renderer* renderer);
|
||||
|
||||
// Avança timers interns (creació + reaccions).
|
||||
// Avança timers interns (creació + ripples).
|
||||
void update(float delta_time);
|
||||
|
||||
// Pinta la graella. La porció dibuixada de cada línia depèn del timer intern.
|
||||
// Completa instantàniament l'animació de creació de la graella (totes les
|
||||
// línies al 100%). Útil per a la demo (attract), que arrenca amb la
|
||||
// partida "ja començada" i no ha de mostrar el muntatge del fons.
|
||||
void completeBuild();
|
||||
|
||||
// Pinta la graella. La porció dibuixada de cada línia depèn del timer intern,
|
||||
// i s'aplica deformació radial per cada ripple activa que afecti la línia.
|
||||
void draw() const;
|
||||
|
||||
// Notifica que una nau ha passat per (pos) a velocitat (speed_px_s).
|
||||
// Si està prop d'alguna línia i va prou ràpida, la línia entra en orbit.
|
||||
void notifyShipPass(Vec2 pos, float speed_px_s);
|
||||
// Notifica que una nau ha passat per (pos) a (speed_px_s). Genera ones
|
||||
// petites darrere la nau a cadència regular amb jitter (estil TrailManager).
|
||||
void notifyShipMoving(std::uint8_t player_id, Vec2 pos, float speed_px_s, float delta_time);
|
||||
|
||||
// Notifica el spawn d'un firework a (pos). Les línies V i H més properes
|
||||
// generen un pulse brillant que es propaga.
|
||||
void notifyFireworkSpawn(Vec2 pos);
|
||||
// Notifica una explosió a (pos): genera una ripple gran centrada al punt.
|
||||
void notifyExplosion(Vec2 pos);
|
||||
|
||||
private:
|
||||
struct Pulse {
|
||||
bool active{false};
|
||||
float center_t{0.5F}; // posició al llarg de la línia (0..1)
|
||||
// Pública per accés des d'helpers a l'anonymous namespace del .cpp.
|
||||
struct Ripple {
|
||||
Vec2 center{};
|
||||
float age_s{0.0F};
|
||||
float lifetime_s{0.0F};
|
||||
float speed_px_s{0.0F};
|
||||
float amplitude_px{0.0F};
|
||||
float thickness_px{0.0F};
|
||||
bool active{false};
|
||||
};
|
||||
|
||||
private:
|
||||
struct Line {
|
||||
Vec2 start; // top (verticals) o left (horitzontals)
|
||||
Vec2 end; // bottom (verticals) o right (horitzontals)
|
||||
float brightness; // base (GRID_BRIGHTNESS o SUBGRID_BRIGHTNESS)
|
||||
float spawn_time_s; // moment de naixement
|
||||
bool is_vertical; // direcció (per saber el perpendicular de l'orbit)
|
||||
float orbit_amplitude; // amplitud actual de l'orbit (px, ≥ 0)
|
||||
float orbit_phase; // fase del sin (avança contínuament)
|
||||
std::array<Pulse, Defaults::Playfield::MAX_PULSES_PER_LINE> pulses;
|
||||
bool is_vertical; // direcció
|
||||
};
|
||||
|
||||
void buildLines();
|
||||
void drawLine(const Line& line, const Ripple* const* active, int n_active) const;
|
||||
[[nodiscard]] auto computeLineProgress(const Line& line) const -> float;
|
||||
static void spawnPulseAt(Line& line, float center_t);
|
||||
void spawnBig(Vec2 pos);
|
||||
void spawnSmall(Vec2 pos);
|
||||
auto findFreeRipple() -> Ripple*;
|
||||
|
||||
Rendering::Renderer* renderer_;
|
||||
std::vector<Line> lines_;
|
||||
std::array<Ripple, Defaults::Playfield::Ripple::POOL_SIZE> ripples_{};
|
||||
std::array<float, 2> ship_ripple_cooldown_{};
|
||||
float elapsed_s_{0.0F};
|
||||
};
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "core/graphics/shape.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
@@ -85,9 +86,23 @@ auto Shape::parseFile(const std::string& contingut) -> bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
bounding_radius_ = computeBoundingRadius(primitives_, center_);
|
||||
return true;
|
||||
}
|
||||
|
||||
auto Shape::computeBoundingRadius(const std::vector<ShapePrimitive>& primitives,
|
||||
const Vec2& center) -> float {
|
||||
float max_dist_sq = 0.0F;
|
||||
for (const auto& prim : primitives) {
|
||||
for (const auto& p : prim.points) {
|
||||
const float DX = p.x - center.x;
|
||||
const float DY = p.y - center.y;
|
||||
max_dist_sq = std::max(max_dist_sq, (DX * DX) + (DY * DY));
|
||||
}
|
||||
}
|
||||
return std::sqrt(max_dist_sq);
|
||||
}
|
||||
|
||||
// Helper: trim whitespace
|
||||
auto Shape::trim(const std::string& str) -> std::string {
|
||||
const char* whitespace = " \t\n\r";
|
||||
|
||||
@@ -42,6 +42,9 @@ class Shape {
|
||||
}
|
||||
[[nodiscard]] auto getCenter() const -> const Vec2& { return center_; }
|
||||
[[nodiscard]] auto getDefaultScale() const -> float { return escala_defecte_; }
|
||||
// Distància màx. del center_ al vèrtex més llunyà; ús: dimensionar
|
||||
// efectes proporcionals a la mida de la shape (halos, glow).
|
||||
[[nodiscard]] auto getBoundingRadius() const -> float { return bounding_radius_; }
|
||||
[[nodiscard]] auto isValid() const -> bool { return !primitives_.empty(); }
|
||||
|
||||
// Info de depuració
|
||||
@@ -53,6 +56,7 @@ class Shape {
|
||||
Vec2 center_; // Centro/origin de la shape
|
||||
float escala_defecte_{1.0F}; // Escala per defecte (normalment 1.0). Inicializada para
|
||||
// que el ctor por defecto no deje el campo indeterminado.
|
||||
float bounding_radius_{0.0F}; // Distància màx. del center_ al vèrtex més llunyà.
|
||||
std::string nom_; // Nom de la shape (per depuració)
|
||||
|
||||
// Helpers privats per parsejar. Son estáticos: no necesitan estado
|
||||
@@ -62,6 +66,9 @@ class Shape {
|
||||
[[nodiscard]] static auto extractValue(const std::string& line) -> std::string;
|
||||
void parseCenter(const std::string& value);
|
||||
[[nodiscard]] static auto parsePoints(const std::string& str) -> std::vector<Vec2>;
|
||||
[[nodiscard]] static auto computeBoundingRadius(
|
||||
const std::vector<ShapePrimitive>& primitives,
|
||||
const Vec2& center) -> float;
|
||||
};
|
||||
|
||||
} // namespace Graphics
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace Graphics {
|
||||
return it->second; // Cache hit
|
||||
}
|
||||
|
||||
// Normalize path: "ship.shp" → "shapes/ship.shp"
|
||||
// Normalize path: "ship/arrow.shp" → "shapes/ship/arrow.shp"
|
||||
// "logo/letra_j.shp" → "shapes/logo/letra_j.shp"
|
||||
std::string normalized = filename;
|
||||
if (!normalized.starts_with("shapes/")) {
|
||||
@@ -53,7 +53,8 @@ namespace Graphics {
|
||||
|
||||
// Cache and return
|
||||
std::cout << "[ShapeLoader] Carregat: " << normalized << " (" << shape->getName()
|
||||
<< ", " << shape->getNumPrimitives() << " primitives)" << '\n';
|
||||
<< ", " << shape->getNumPrimitives() << " primitives, bounding_radius="
|
||||
<< shape->getBoundingRadius() << ")" << '\n';
|
||||
|
||||
cache[filename] = shape;
|
||||
return shape;
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace Graphics {
|
||||
|
||||
// Carregar shape desde file (con caché)
|
||||
// Retorna punter compartit (nullptr si error)
|
||||
// Exemple: load("ship.shp") → busca a "data/shapes/ship.shp"
|
||||
// Exemple: load("ship/arrow.shp") → busca a "data/shapes/ship/arrow.shp"
|
||||
static auto load(const std::string& filename) -> std::shared_ptr<Shape>;
|
||||
|
||||
// Netejar caché (útil per debug/recàrrega)
|
||||
|
||||
@@ -1,168 +1,109 @@
|
||||
// starfield.cpp - Implementació del sistema de estrelles de fons
|
||||
// starfield.cpp - Implementació del starfield 3D
|
||||
// © 2026 JailDesigner
|
||||
|
||||
#include "core/graphics/starfield.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
|
||||
#include "core/defaults.hpp"
|
||||
#include "core/graphics/shape_loader.hpp"
|
||||
#include "core/rendering/shape_renderer.hpp"
|
||||
|
||||
namespace Graphics {
|
||||
|
||||
// Constructor
|
||||
Starfield::Starfield(Rendering::Renderer* renderer,
|
||||
const Vec2& punt_fuga,
|
||||
const SDL_FRect& area,
|
||||
int densitat)
|
||||
: shape_estrella_(ShapeLoader::load("star.shp")),
|
||||
renderer_(renderer),
|
||||
punt_fuga_(punt_fuga),
|
||||
area_(area) {
|
||||
if (!shape_estrella_ || !shape_estrella_->isValid()) {
|
||||
std::cerr << "ERROR: No s'ha pogut load star.shp" << '\n';
|
||||
return;
|
||||
namespace {
|
||||
|
||||
// Helper: número aleatori en [0, 1) usant rand()/RAND_MAX (mateixa convenció
|
||||
// que la resta del joc — veure starfield.cpp).
|
||||
auto randFloat01() -> float {
|
||||
return static_cast<float>(rand()) / static_cast<float>(RAND_MAX);
|
||||
}
|
||||
|
||||
// Configurar 3 capes con diferents velocitats i escales
|
||||
// Capa 0: Fons llunyà (lenta, pequeña)
|
||||
capes_.push_back({20.0F, 0.3F, 0.8F, densitat / 3});
|
||||
|
||||
// Capa 1: Profunditat mitjana
|
||||
capes_.push_back({40.0F, 0.5F, 1.2F, densitat / 3});
|
||||
|
||||
// Capa 2: Primer pla (ràpida, grande)
|
||||
capes_.push_back({80.0F, 0.8F, 2.0F, densitat / 3});
|
||||
|
||||
// Calcular radi màxim (distancia del centro al racó més llunyà)
|
||||
float dx = std::max(punt_fuga_.x, area_.w - punt_fuga_.x);
|
||||
float dy = std::max(punt_fuga_.y, area_.h - punt_fuga_.y);
|
||||
radi_max_ = std::sqrt((dx * dx) + (dy * dy));
|
||||
|
||||
// Inicialitzar estrelles con posicions distribuïdes (pre-omplir pantalla)
|
||||
for (int capa_idx = 0; capa_idx < 3; capa_idx++) {
|
||||
int num = capes_[capa_idx].num_estrelles;
|
||||
for (int i = 0; i < num; i++) {
|
||||
Estrella estrella;
|
||||
estrella.capa = capa_idx;
|
||||
|
||||
// Angle aleatori
|
||||
estrella.angle = (static_cast<float>(rand()) / static_cast<float>(RAND_MAX)) * 2.0F * Defaults::Math::PI;
|
||||
|
||||
// Distancia aleatòria (0.0 a 1.0) per omplir toda la pantalla
|
||||
estrella.distancia_centre = static_cast<float>(rand()) / static_cast<float>(RAND_MAX);
|
||||
|
||||
// Calcular posición desde la distancia
|
||||
float radi = estrella.distancia_centre * radi_max_;
|
||||
estrella.position.x = punt_fuga_.x + (radi * std::cos(estrella.angle));
|
||||
estrella.position.y = punt_fuga_.y + (radi * std::sin(estrella.angle));
|
||||
|
||||
estrelles_.push_back(estrella);
|
||||
}
|
||||
}
|
||||
auto randRange(float lo, float hi) -> float {
|
||||
return lo + (randFloat01() * (hi - lo));
|
||||
}
|
||||
|
||||
// Inicialitzar una estrella (nueva o regenerada)
|
||||
void Starfield::initStar(Estrella& estrella) const {
|
||||
// Angle aleatori des del point de fuga hacia fuera
|
||||
estrella.angle = (static_cast<float>(rand()) / static_cast<float>(RAND_MAX)) * 2.0F * Defaults::Math::PI;
|
||||
} // namespace
|
||||
|
||||
// Distancia inicial pequeña (5% del radi màxim) - neix prop del centro
|
||||
estrella.distancia_centre = 0.05F;
|
||||
|
||||
// Posición inicial: mucho prop del point de fuga
|
||||
float radi = estrella.distancia_centre * radi_max_;
|
||||
estrella.position.x = punt_fuga_.x + (radi * std::cos(estrella.angle));
|
||||
estrella.position.y = punt_fuga_.y + (radi * std::sin(estrella.angle));
|
||||
Starfield::Starfield(Rendering::Renderer* renderer, const Camera3D* camera, int density)
|
||||
: renderer_(renderer),
|
||||
camera_(camera),
|
||||
octahedron_(makeOctahedron()) {
|
||||
stars_.resize(static_cast<std::size_t>(std::max(0, density)));
|
||||
for (auto& star : stars_) {
|
||||
// Pre-omplir amb estrelles distribuïdes per tot el rang Z, no només al far.
|
||||
initStar(star, /*spawn_at_far=*/false);
|
||||
}
|
||||
}
|
||||
|
||||
// Verificar si una estrella está fuera de l'àrea
|
||||
auto Starfield::isOutsideArea(const Estrella& estrella) const -> bool {
|
||||
return (estrella.position.x < area_.x ||
|
||||
estrella.position.x > area_.x + area_.w ||
|
||||
estrella.position.y < area_.y ||
|
||||
estrella.position.y > area_.y + area_.h);
|
||||
void Starfield::initStar(Star& star, bool spawn_at_far) {
|
||||
star.position.x = randRange(-HALF_SPAWN_X, HALF_SPAWN_X);
|
||||
star.position.y = randRange(-HALF_SPAWN_Y, HALF_SPAWN_Y);
|
||||
star.position.z = spawn_at_far
|
||||
? Z_FAR_SPAWN
|
||||
: randRange(Z_NEAR_RESPAWN, Z_FAR_SPAWN);
|
||||
|
||||
star.velocity_z = -randRange(MIN_VELOCITY_Z, MAX_VELOCITY_Z);
|
||||
star.rot_phase_y = randFloat01() * Defaults::Math::PI * 2.0F;
|
||||
star.rot_phase_x = randFloat01() * Defaults::Math::PI * 2.0F;
|
||||
star.rot_speed_y = randRange(MIN_ROT_SPEED, MAX_ROT_SPEED);
|
||||
star.rot_speed_x = randRange(MIN_ROT_SPEED, MAX_ROT_SPEED);
|
||||
star.scale = STAR_BASE_SCALE + (randRange(-1.0F, 1.0F) * STAR_SCALE_JITTER);
|
||||
}
|
||||
|
||||
// Calcular scale dinàmica segons distancia del centro
|
||||
auto Starfield::computeScale(const Estrella& estrella) const -> float {
|
||||
const CapaConfig& capa = capes_[estrella.capa];
|
||||
|
||||
// Interpolació lineal basada en distancia del centro
|
||||
// distancia_centre: 0.0 (centro) → 1.0 (vora)
|
||||
return capa.escala_min +
|
||||
((capa.escala_max - capa.escala_min) * estrella.distancia_centre);
|
||||
auto Starfield::computeBrightness(const Star& star) const -> float {
|
||||
// Lerp segons distància Z normalitzada [Z_NEAR_RESPAWN .. Z_FAR_SPAWN].
|
||||
const float SPAN = Z_FAR_SPAWN - Z_NEAR_RESPAWN;
|
||||
const float T_RAW = (star.position.z - Z_NEAR_RESPAWN) / SPAN;
|
||||
const float T = std::clamp(T_RAW, 0.0F, 1.0F);
|
||||
const float BRIGHTNESS = BRIGHTNESS_NEAR + (T * (BRIGHTNESS_FAR - BRIGHTNESS_NEAR));
|
||||
return std::clamp(BRIGHTNESS * brightness_mult_, 0.0F, 1.0F);
|
||||
}
|
||||
|
||||
// Calcular brightness dinàmica segons distancia del centro
|
||||
auto Starfield::computeBrightness(const Estrella& estrella) const -> float {
|
||||
// Interpolació lineal: estrelles properes (vora) més brillants
|
||||
// distancia_centre: 0.0 (centro, llunyanes) → 1.0 (vora, properes)
|
||||
float brightness_base = Defaults::Brightness::STARFIELD_MIN +
|
||||
((Defaults::Brightness::STARFIELD_MAX - Defaults::Brightness::STARFIELD_MIN) *
|
||||
estrella.distancia_centre);
|
||||
|
||||
// Aplicar multiplicador i limitar a 1.0
|
||||
return std::min(1.0F, brightness_base * multiplicador_brightness_);
|
||||
}
|
||||
|
||||
// Actualitzar posicions de las estrelles
|
||||
void Starfield::update(float delta_time) {
|
||||
for (auto& estrella : estrelles_) {
|
||||
// Obtenir configuración de la capa
|
||||
const CapaConfig& capa = capes_[estrella.capa];
|
||||
for (auto& star : stars_) {
|
||||
star.position.z += star.velocity_z * delta_time;
|
||||
star.rot_phase_y += star.rot_speed_y * delta_time;
|
||||
star.rot_phase_x += star.rot_speed_x * delta_time;
|
||||
|
||||
// Moure hacia fuera des del centro
|
||||
float velocity = capa.velocitat_base;
|
||||
float dx = velocity * std::cos(estrella.angle) * delta_time;
|
||||
float dy = velocity * std::sin(estrella.angle) * delta_time;
|
||||
|
||||
estrella.position.x += dx;
|
||||
estrella.position.y += dy;
|
||||
|
||||
// Actualitzar distancia del centro
|
||||
float dx_centre = estrella.position.x - punt_fuga_.x;
|
||||
float dy_centre = estrella.position.y - punt_fuga_.y;
|
||||
float dist_px = std::sqrt((dx_centre * dx_centre) + (dy_centre * dy_centre));
|
||||
estrella.distancia_centre = dist_px / radi_max_;
|
||||
|
||||
// Si ha sortit de l'àrea, regenerar-la
|
||||
if (isOutsideArea(estrella)) {
|
||||
initStar(estrella);
|
||||
if (star.position.z < Z_NEAR_RESPAWN) {
|
||||
initStar(star, /*spawn_at_far=*/true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Establir multiplicador de brightness
|
||||
void Starfield::setBrightness(float multiplier) {
|
||||
multiplicador_brightness_ = std::max(0.0F, multiplier); // Evitar valors negatius
|
||||
}
|
||||
|
||||
// Dibuixar todas las estrelles
|
||||
void Starfield::draw() {
|
||||
if (!shape_estrella_->isValid()) {
|
||||
void Starfield::draw() const {
|
||||
if (camera_ == nullptr || renderer_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto& estrella : estrelles_) {
|
||||
// Calcular scale i brightness dinàmicament
|
||||
float scale = computeScale(estrella);
|
||||
float brightness = computeBrightness(estrella);
|
||||
|
||||
// Renderizar estrella sin rotación
|
||||
Rendering::renderShape(
|
||||
renderer_,
|
||||
shape_estrella_,
|
||||
estrella.position,
|
||||
0.0F, // angle (las estrelles no giren)
|
||||
scale, // scale dinàmica
|
||||
1.0F, // progress (siempre visible)
|
||||
brightness // brightness dinàmica
|
||||
);
|
||||
// Ordena de més lluny a més a prop perquè el blending additiu acumule
|
||||
// brightness sense que els elements de davant queden tapats pels de
|
||||
// darrere. Còpia d'índexs per no modificar l'ordre intern de stars_.
|
||||
std::vector<std::size_t> order(stars_.size());
|
||||
for (std::size_t i = 0; i < order.size(); ++i) {
|
||||
order[i] = i;
|
||||
}
|
||||
std::ranges::sort(order, [&](std::size_t a, std::size_t b) {
|
||||
return stars_[a].position.z > stars_[b].position.z;
|
||||
});
|
||||
|
||||
for (std::size_t idx : order) {
|
||||
const Star& star = stars_[idx];
|
||||
const Transform3D TRANSFORM{
|
||||
.position = star.position,
|
||||
.rotation_euler = Vec3{.x = star.rot_phase_x, .y = star.rot_phase_y, .z = 0.0F},
|
||||
.scale = star.scale,
|
||||
};
|
||||
drawWireframe(renderer_, *camera_, octahedron_, TRANSFORM, computeBrightness(star), color_);
|
||||
}
|
||||
}
|
||||
|
||||
void Starfield::setBrightness(float multiplier) {
|
||||
brightness_mult_ = std::max(0.0F, multiplier);
|
||||
}
|
||||
|
||||
void Starfield::setColor(SDL_Color color) {
|
||||
color_ = color;
|
||||
}
|
||||
|
||||
} // namespace Graphics
|
||||
|
||||
@@ -1,83 +1,72 @@
|
||||
// starfield.hpp - Sistema de estrelles de fons con efecte de profunditat
|
||||
// starfield.hpp - Camp d'estrelles 3D per a l'escena de títol
|
||||
// © 2026 JailDesigner
|
||||
//
|
||||
// Cada estrella és un octaedre
|
||||
// wireframe situat en l'espai mundial (Vec3). Es desplacen cap a la càmera
|
||||
// (Z disminueix); quan creuen el pla Z_NEAR_RESPAWN es regeneren a Z_FAR_SPAWN
|
||||
// amb X/Y aleatori. Cada octaedre rota lentament sobre Y i X per donar volum.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "core/rendering/render_context.hpp"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "core/graphics/shape.hpp"
|
||||
#include "core/graphics/camera3d.hpp"
|
||||
#include "core/graphics/wireframe3d.hpp"
|
||||
#include "core/rendering/render_context.hpp"
|
||||
#include "core/types.hpp"
|
||||
|
||||
namespace Graphics {
|
||||
|
||||
// Configuración per cada capa de profunditat
|
||||
struct CapaConfig {
|
||||
float velocitat_base; // Velocidad base de esta capa (px/s)
|
||||
float escala_min; // Escala mínima prop del centro
|
||||
float escala_max; // Escala màxima al límit de pantalla
|
||||
int num_estrelles; // Nombre de estrelles en esta capa
|
||||
};
|
||||
|
||||
// Clase Starfield - camp de estrelles animat con efecte de profunditat
|
||||
class Starfield {
|
||||
public:
|
||||
// Constructor
|
||||
// - renderer: SDL renderer
|
||||
// - punt_fuga: point de origin/fuga des de on surten las estrelles
|
||||
// - area: rectangle on actuen las estrelles (SDL_FRect)
|
||||
// - densitat: nombre total de estrelles (es divideix entre capes)
|
||||
Starfield(Rendering::Renderer* renderer,
|
||||
const Vec2& punt_fuga,
|
||||
const SDL_FRect& area,
|
||||
int densitat = 150);
|
||||
Starfield(Rendering::Renderer* renderer, const Camera3D* camera, int density = 200);
|
||||
|
||||
// Actualitzar posicions de las estrelles
|
||||
void update(float delta_time);
|
||||
void draw() const;
|
||||
|
||||
// Dibuixar todas las estrelles
|
||||
void draw();
|
||||
|
||||
// Setters per ajustar parámetros en time real
|
||||
void setVanishingPoint(const Vec2& point) { punt_fuga_ = point; }
|
||||
void setBrightness(float multiplier);
|
||||
void setColor(SDL_Color color);
|
||||
|
||||
private:
|
||||
// Estructura interna per cada estrella
|
||||
struct Estrella {
|
||||
Vec2 position; // Posición actual
|
||||
float angle; // Angle de movement (radians)
|
||||
float distancia_centre; // Distancia normalitzada del centro (0.0-1.0)
|
||||
int capa; // Índex de capa (0=lluny, 1=mitjà, 2=prop)
|
||||
struct Star {
|
||||
Vec3 position{};
|
||||
float velocity_z{0.0F}; // Negatiu: cap a càmera
|
||||
float rot_phase_y{0.0F};
|
||||
float rot_phase_x{0.0F};
|
||||
float rot_speed_y{0.0F};
|
||||
float rot_speed_x{0.0F};
|
||||
float scale{1.0F};
|
||||
};
|
||||
|
||||
// Inicialitzar una estrella (nueva o regenerada)
|
||||
void initStar(Estrella& estrella) const;
|
||||
static void initStar(Star& star, bool spawn_at_far);
|
||||
[[nodiscard]] auto computeBrightness(const Star& star) const -> float;
|
||||
|
||||
// Verificar si una estrella está fuera de l'àrea
|
||||
[[nodiscard]] auto isOutsideArea(const Estrella& estrella) const -> bool;
|
||||
|
||||
// Calcular scale dinàmica segons distancia del centro
|
||||
[[nodiscard]] auto computeScale(const Estrella& estrella) const -> float;
|
||||
|
||||
// Calcular brightness dinàmica segons distancia del centro
|
||||
[[nodiscard]] auto computeBrightness(const Estrella& estrella) const -> float;
|
||||
|
||||
// Dades
|
||||
std::vector<Estrella> estrelles_;
|
||||
std::vector<CapaConfig> capes_; // Configuración de las 3 capes
|
||||
std::shared_ptr<Shape> shape_estrella_;
|
||||
Rendering::Renderer* renderer_;
|
||||
const Camera3D* camera_;
|
||||
std::vector<Star> stars_;
|
||||
Mesh3D octahedron_;
|
||||
float brightness_mult_{1.0F};
|
||||
SDL_Color color_{.r = 0, .g = 0, .b = 0, .a = 0}; // alpha=0 → usa color global
|
||||
|
||||
// Configuración
|
||||
Vec2 punt_fuga_; // Vec2 de origin de las estrelles
|
||||
SDL_FRect area_; // Àrea activa
|
||||
float radi_max_; // Distancia màxima del centro al límit de pantalla
|
||||
float multiplicador_brightness_{1.0F}; // Multiplicador de brightness (1.0 = default)
|
||||
// Volum de spawn / regeneració en l'espai 3D.
|
||||
static constexpr float Z_NEAR_RESPAWN = 5.0F; // Si Z < aquest valor → regenera
|
||||
static constexpr float Z_FAR_SPAWN = 1500.0F; // Z de regeneració (lluny — més profunditat)
|
||||
static constexpr float HALF_SPAWN_X = 900.0F; // X aleatori dins [-, +]
|
||||
static constexpr float HALF_SPAWN_Y = 540.0F; // Y aleatori dins [-, +]
|
||||
|
||||
// Mida i moviment.
|
||||
static constexpr float STAR_BASE_SCALE = 1.8F;
|
||||
static constexpr float STAR_SCALE_JITTER = 0.6F;
|
||||
static constexpr float MIN_VELOCITY_Z = 80.0F;
|
||||
static constexpr float MAX_VELOCITY_Z = 200.0F;
|
||||
static constexpr float MIN_ROT_SPEED = 0.2F;
|
||||
static constexpr float MAX_ROT_SPEED = 0.8F;
|
||||
|
||||
// Brightness en funció de la distància Z (a prop = més brillant).
|
||||
static constexpr float BRIGHTNESS_FAR = 0.15F;
|
||||
static constexpr float BRIGHTNESS_NEAR = 1.0F;
|
||||
};
|
||||
|
||||
} // namespace Graphics
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
// starfield3d.cpp - Implementació del starfield 3D
|
||||
// © 2026 JailDesigner
|
||||
|
||||
#include "core/graphics/starfield3d.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
|
||||
#include "core/defaults.hpp"
|
||||
|
||||
namespace Graphics {
|
||||
|
||||
namespace {
|
||||
|
||||
// Helper: número aleatori en [0, 1) usant rand()/RAND_MAX (mateixa convenció
|
||||
// que la resta del joc — veure starfield.cpp).
|
||||
auto randFloat01() -> float {
|
||||
return static_cast<float>(rand()) / static_cast<float>(RAND_MAX);
|
||||
}
|
||||
|
||||
auto randRange(float lo, float hi) -> float {
|
||||
return lo + (randFloat01() * (hi - lo));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Starfield3D::Starfield3D(Rendering::Renderer* renderer, const Camera3D* camera, int density)
|
||||
: renderer_(renderer),
|
||||
camera_(camera),
|
||||
octahedron_(makeOctahedron()) {
|
||||
stars_.resize(static_cast<std::size_t>(std::max(0, density)));
|
||||
for (auto& star : stars_) {
|
||||
// Pre-omplir amb estrelles distribuïdes per tot el rang Z, no només al far.
|
||||
initStar(star, /*spawn_at_far=*/false);
|
||||
}
|
||||
}
|
||||
|
||||
void Starfield3D::initStar(Star& star, bool spawn_at_far) {
|
||||
star.position.x = randRange(-HALF_SPAWN_X, HALF_SPAWN_X);
|
||||
star.position.y = randRange(-HALF_SPAWN_Y, HALF_SPAWN_Y);
|
||||
star.position.z = spawn_at_far
|
||||
? Z_FAR_SPAWN
|
||||
: randRange(Z_NEAR_RESPAWN, Z_FAR_SPAWN);
|
||||
|
||||
star.velocity_z = -randRange(MIN_VELOCITY_Z, MAX_VELOCITY_Z);
|
||||
star.rot_phase_y = randFloat01() * Defaults::Math::PI * 2.0F;
|
||||
star.rot_phase_x = randFloat01() * Defaults::Math::PI * 2.0F;
|
||||
star.rot_speed_y = randRange(MIN_ROT_SPEED, MAX_ROT_SPEED);
|
||||
star.rot_speed_x = randRange(MIN_ROT_SPEED, MAX_ROT_SPEED);
|
||||
star.scale = STAR_BASE_SCALE + (randRange(-1.0F, 1.0F) * STAR_SCALE_JITTER);
|
||||
}
|
||||
|
||||
auto Starfield3D::computeBrightness(const Star& star) const -> float {
|
||||
// Lerp segons distància Z normalitzada [Z_NEAR_RESPAWN .. Z_FAR_SPAWN].
|
||||
const float SPAN = Z_FAR_SPAWN - Z_NEAR_RESPAWN;
|
||||
const float T_RAW = (star.position.z - Z_NEAR_RESPAWN) / SPAN;
|
||||
const float T = std::clamp(T_RAW, 0.0F, 1.0F);
|
||||
const float BRIGHTNESS = BRIGHTNESS_NEAR + (T * (BRIGHTNESS_FAR - BRIGHTNESS_NEAR));
|
||||
return std::clamp(BRIGHTNESS * brightness_mult_, 0.0F, 1.0F);
|
||||
}
|
||||
|
||||
void Starfield3D::update(float delta_time) {
|
||||
for (auto& star : stars_) {
|
||||
star.position.z += star.velocity_z * delta_time;
|
||||
star.rot_phase_y += star.rot_speed_y * delta_time;
|
||||
star.rot_phase_x += star.rot_speed_x * delta_time;
|
||||
|
||||
if (star.position.z < Z_NEAR_RESPAWN) {
|
||||
initStar(star, /*spawn_at_far=*/true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Starfield3D::draw() const {
|
||||
if (camera_ == nullptr || renderer_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
// Ordena de més lluny a més a prop perquè el blending additiu acumule
|
||||
// brightness sense que els elements de davant queden tapats pels de
|
||||
// darrere. Còpia d'índexs per no modificar l'ordre intern de stars_.
|
||||
std::vector<std::size_t> order(stars_.size());
|
||||
for (std::size_t i = 0; i < order.size(); ++i) {
|
||||
order[i] = i;
|
||||
}
|
||||
std::ranges::sort(order, [&](std::size_t a, std::size_t b) {
|
||||
return stars_[a].position.z > stars_[b].position.z;
|
||||
});
|
||||
|
||||
for (std::size_t idx : order) {
|
||||
const Star& star = stars_[idx];
|
||||
const Transform3D TRANSFORM{
|
||||
.position = star.position,
|
||||
.rotation_euler = Vec3{.x = star.rot_phase_x, .y = star.rot_phase_y, .z = 0.0F},
|
||||
.scale = star.scale,
|
||||
};
|
||||
drawWireframe(renderer_, *camera_, octahedron_, TRANSFORM, computeBrightness(star));
|
||||
}
|
||||
}
|
||||
|
||||
void Starfield3D::setBrightness(float multiplier) {
|
||||
brightness_mult_ = std::max(0.0F, multiplier);
|
||||
}
|
||||
|
||||
} // namespace Graphics
|
||||
@@ -1,68 +0,0 @@
|
||||
// starfield3d.hpp - Camp de estrelles 3D real per a l'escena de títol
|
||||
// © 2026 JailDesigner
|
||||
//
|
||||
// Equivalent 3D del `Graphics::Starfield`. Cada estrella és un octaedre
|
||||
// wireframe situat en l'espai mundial (Vec3). Es desplacen cap a la càmera
|
||||
// (Z disminueix); quan creuen el pla Z_NEAR_RESPAWN es regeneren a Z_FAR_SPAWN
|
||||
// amb X/Y aleatori. Cada octaedre rota lentament sobre Y i X per donar volum.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "core/graphics/camera3d.hpp"
|
||||
#include "core/graphics/wireframe3d.hpp"
|
||||
#include "core/rendering/render_context.hpp"
|
||||
#include "core/types.hpp"
|
||||
|
||||
namespace Graphics {
|
||||
|
||||
class Starfield3D {
|
||||
public:
|
||||
Starfield3D(Rendering::Renderer* renderer, const Camera3D* camera, int density = 200);
|
||||
|
||||
void update(float delta_time);
|
||||
void draw() const;
|
||||
|
||||
void setBrightness(float multiplier);
|
||||
|
||||
private:
|
||||
struct Star {
|
||||
Vec3 position{};
|
||||
float velocity_z{0.0F}; // Negatiu: cap a càmera
|
||||
float rot_phase_y{0.0F};
|
||||
float rot_phase_x{0.0F};
|
||||
float rot_speed_y{0.0F};
|
||||
float rot_speed_x{0.0F};
|
||||
float scale{1.0F};
|
||||
};
|
||||
|
||||
static void initStar(Star& star, bool spawn_at_far);
|
||||
[[nodiscard]] auto computeBrightness(const Star& star) const -> float;
|
||||
|
||||
Rendering::Renderer* renderer_;
|
||||
const Camera3D* camera_;
|
||||
std::vector<Star> stars_;
|
||||
Mesh3D octahedron_;
|
||||
float brightness_mult_{1.0F};
|
||||
|
||||
// Volum de spawn / regeneració en l'espai 3D.
|
||||
static constexpr float Z_NEAR_RESPAWN = 5.0F; // Si Z < aquest valor → regenera
|
||||
static constexpr float Z_FAR_SPAWN = 800.0F; // Z de regeneració (lluny)
|
||||
static constexpr float HALF_SPAWN_X = 600.0F; // X aleatori dins [-, +]
|
||||
static constexpr float HALF_SPAWN_Y = 360.0F; // Y aleatori dins [-, +]
|
||||
|
||||
// Mida i moviment.
|
||||
static constexpr float STAR_BASE_SCALE = 1.8F;
|
||||
static constexpr float STAR_SCALE_JITTER = 0.6F;
|
||||
static constexpr float MIN_VELOCITY_Z = 80.0F;
|
||||
static constexpr float MAX_VELOCITY_Z = 200.0F;
|
||||
static constexpr float MIN_ROT_SPEED = 0.2F;
|
||||
static constexpr float MAX_ROT_SPEED = 0.8F;
|
||||
|
||||
// Brightness en funció de la distància Z (a prop = més brillant).
|
||||
static constexpr float BRIGHTNESS_FAR = 0.15F;
|
||||
static constexpr float BRIGHTNESS_NEAR = 1.0F;
|
||||
};
|
||||
|
||||
} // namespace Graphics
|
||||
@@ -0,0 +1,140 @@
|
||||
// starfield_parallax.cpp - Implementació del starfield 2D amb parallax
|
||||
// © 2026 JailDesigner
|
||||
|
||||
#include "core/graphics/starfield_parallax.hpp"
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
#include "core/defaults.hpp"
|
||||
#include "core/rendering/line_renderer.hpp"
|
||||
|
||||
namespace Graphics {
|
||||
|
||||
namespace {
|
||||
|
||||
auto randUniform(float min_v, float max_v) -> float {
|
||||
const float NORM = static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX);
|
||||
return min_v + (NORM * (max_v - min_v));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
StarfieldParallax::StarfieldParallax(Rendering::Renderer* renderer)
|
||||
: renderer_(renderer) {
|
||||
buildStars();
|
||||
}
|
||||
|
||||
void StarfieldParallax::buildStars() {
|
||||
const SDL_FRect& zone = Defaults::Zones::PLAYAREA;
|
||||
const float MIN_X = zone.x;
|
||||
const float MAX_X = zone.x + zone.w;
|
||||
const float MIN_Y = zone.y;
|
||||
const float MAX_Y = zone.y + zone.h;
|
||||
|
||||
// Color únic per a totes les estrelles: el mateix blanc-blau gel
|
||||
// del starfield del títol (Defaults::Title::Colors::STARFIELD).
|
||||
const auto FILL_LAYER = [&](int layer, int count, int& idx) {
|
||||
for (int i = 0; i < count; i++) {
|
||||
stars_[idx++] = Star{
|
||||
.x = randUniform(MIN_X, MAX_X),
|
||||
.y = randUniform(MIN_Y, MAX_Y),
|
||||
.layer = layer,
|
||||
.color = Defaults::Title::Colors::STARFIELD};
|
||||
}
|
||||
};
|
||||
|
||||
int idx = 0;
|
||||
FILL_LAYER(0, Defaults::StarfieldParallax::Far::COUNT, idx);
|
||||
FILL_LAYER(1, Defaults::StarfieldParallax::Mid::COUNT, idx);
|
||||
FILL_LAYER(2, Defaults::StarfieldParallax::Near::COUNT, idx);
|
||||
}
|
||||
|
||||
auto StarfieldParallax::layerBrightness(int layer) -> float {
|
||||
switch (layer) {
|
||||
case 0:
|
||||
return Defaults::StarfieldParallax::Far::BRIGHTNESS;
|
||||
case 1:
|
||||
return Defaults::StarfieldParallax::Mid::BRIGHTNESS;
|
||||
case 2:
|
||||
return Defaults::StarfieldParallax::Near::BRIGHTNESS;
|
||||
default:
|
||||
return 0.0F;
|
||||
}
|
||||
}
|
||||
|
||||
auto StarfieldParallax::layerParallax(int layer) -> float {
|
||||
switch (layer) {
|
||||
case 0:
|
||||
return Defaults::StarfieldParallax::Far::PARALLAX_FACTOR;
|
||||
case 1:
|
||||
return Defaults::StarfieldParallax::Mid::PARALLAX_FACTOR;
|
||||
case 2:
|
||||
return Defaults::StarfieldParallax::Near::PARALLAX_FACTOR;
|
||||
default:
|
||||
return 0.0F;
|
||||
}
|
||||
}
|
||||
|
||||
auto StarfieldParallax::layerSize(int layer) -> int {
|
||||
switch (layer) {
|
||||
case 0:
|
||||
return Defaults::StarfieldParallax::Far::SIZE_PX;
|
||||
case 1:
|
||||
return Defaults::StarfieldParallax::Mid::SIZE_PX;
|
||||
case 2:
|
||||
return Defaults::StarfieldParallax::Near::SIZE_PX;
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
void StarfieldParallax::update(float delta_time, Vec2 world_velocity) {
|
||||
const SDL_FRect& zone = Defaults::Zones::PLAYAREA;
|
||||
const float MIN_X = zone.x;
|
||||
const float MAX_X = zone.x + zone.w;
|
||||
const float MIN_Y = zone.y;
|
||||
const float MAX_Y = zone.y + zone.h;
|
||||
const float W = zone.w;
|
||||
const float H = zone.h;
|
||||
|
||||
for (auto& star : stars_) {
|
||||
const float FACTOR = layerParallax(star.layer);
|
||||
star.x += world_velocity.x * FACTOR * delta_time;
|
||||
star.y += world_velocity.y * FACTOR * delta_time;
|
||||
|
||||
// Wraparound (PLAYAREA torica).
|
||||
while (star.x < MIN_X) {
|
||||
star.x += W;
|
||||
}
|
||||
while (star.x > MAX_X) {
|
||||
star.x -= W;
|
||||
}
|
||||
while (star.y < MIN_Y) {
|
||||
star.y += H;
|
||||
}
|
||||
while (star.y > MAX_Y) {
|
||||
star.y -= H;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void StarfieldParallax::draw() const {
|
||||
for (const auto& star : stars_) {
|
||||
const float B = layerBrightness(star.layer);
|
||||
const int SIZE = layerSize(star.layer);
|
||||
const int X = static_cast<int>(star.x);
|
||||
const int Y = static_cast<int>(star.y);
|
||||
|
||||
if (SIZE <= 1) {
|
||||
// Punt d'1 px: línia degenerada horitzontal de 1 px.
|
||||
Rendering::linea(renderer_, X, Y, X + 1, Y, B, 0.0F, star.color);
|
||||
} else {
|
||||
// Creu "+" amb extensió HALF des del centre en cada direcció.
|
||||
const int HALF = SIZE - 1; // SIZE=2 → ±1 (creu 3x3); SIZE=3 → ±2 (creu 5x5)
|
||||
Rendering::linea(renderer_, X - HALF, Y, X + HALF + 1, Y, B, 0.0F, star.color);
|
||||
Rendering::linea(renderer_, X, Y - HALF, X, Y + HALF + 1, B, 0.0F, star.color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Graphics
|
||||
@@ -0,0 +1,51 @@
|
||||
// starfield_parallax.hpp - Capa més profunda del fons: estrelles 2D amb parallax
|
||||
// © 2026 JailDesigner
|
||||
//
|
||||
// Estrelles 2D distribuïdes en 3 capes de profunditat. Cada capa té el seu
|
||||
// factor parallax: el "món" es desplaça amb world_velocity i les estrelles
|
||||
// d'una capa es mouen amb world_velocity * parallax_factor. Les més
|
||||
// properes es mouen més (factor alt) → sensació de profunditat.
|
||||
// Quan una estrella surt de PLAYAREA, reapareix per la banda oposada
|
||||
// (wraparound).
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <array>
|
||||
|
||||
#include "core/defaults/starfield_parallax.hpp"
|
||||
#include "core/rendering/render_context.hpp"
|
||||
#include "core/types.hpp"
|
||||
|
||||
namespace Graphics {
|
||||
|
||||
class StarfieldParallax {
|
||||
public:
|
||||
explicit StarfieldParallax(Rendering::Renderer* renderer);
|
||||
|
||||
// Avança el desplaçament de les estrelles segons world_velocity (vector
|
||||
// del moviment del món en px/s; típicament = -ship_velocity).
|
||||
// world_velocity == {0, 0} → estrelles quietes.
|
||||
void update(float delta_time, Vec2 world_velocity);
|
||||
|
||||
void draw() const;
|
||||
|
||||
private:
|
||||
struct Star {
|
||||
float x{0.0F};
|
||||
float y{0.0F};
|
||||
int layer{0}; // 0=Far, 1=Mid, 2=Near
|
||||
SDL_Color color{}; // tint precomputat entre blanc i cyan
|
||||
};
|
||||
|
||||
void buildStars();
|
||||
static auto layerBrightness(int layer) -> float;
|
||||
static auto layerParallax(int layer) -> float;
|
||||
static auto layerSize(int layer) -> int;
|
||||
|
||||
Rendering::Renderer* renderer_;
|
||||
std::array<Star, Defaults::StarfieldParallax::TOTAL_COUNT> stars_{};
|
||||
};
|
||||
|
||||
} // namespace Graphics
|
||||
@@ -11,8 +11,9 @@
|
||||
namespace Graphics {
|
||||
|
||||
// Constants para mides base dels caràcters
|
||||
constexpr float BASE_CHAR_WIDTH = 20.0F; // Amplada base del caràcter
|
||||
constexpr float BASE_CHAR_HEIGHT = 40.0F; // Altura base del caràcter
|
||||
constexpr float BASE_CHAR_WIDTH = 20.0F; // Amplada base del caràcter (cel·la)
|
||||
constexpr float BASE_CHAR_HEIGHT = 40.0F; // Altura base del caràcter (cel·la, amb marge)
|
||||
constexpr float BASE_GLYPH_HEIGHT = 20.0F; // Altura real del glif (la majúscula/dígit ocupa 20 dels 40)
|
||||
|
||||
VectorText::VectorText(Rendering::Renderer* renderer)
|
||||
: renderer_(renderer) {
|
||||
@@ -47,7 +48,7 @@ namespace Graphics {
|
||||
}
|
||||
|
||||
// Cargar símbolos
|
||||
const std::string SYMBOLS[] = {".", ",", "-", ":", "!", "?"};
|
||||
const std::string SYMBOLS[] = {".", ",", "-", ":", "!", "?", "/", "(", ")", "_"};
|
||||
for (const auto& sym : SYMBOLS) {
|
||||
char c = sym[0];
|
||||
std::string filename = getShapeFilename(c);
|
||||
@@ -164,6 +165,14 @@ namespace Graphics {
|
||||
return "font/char_exclamation.shp";
|
||||
case '?':
|
||||
return "font/char_question.shp";
|
||||
case '/':
|
||||
return "font/char_slash.shp";
|
||||
case '(':
|
||||
return "font/char_lparen.shp";
|
||||
case ')':
|
||||
return "font/char_rparen.shp";
|
||||
case '_':
|
||||
return "font/char_underscore.shp";
|
||||
case ' ':
|
||||
return ""; // Espai es maneja sin load shape
|
||||
|
||||
@@ -176,6 +185,10 @@ namespace Graphics {
|
||||
}
|
||||
|
||||
auto VectorText::isSupported(char c) const -> bool {
|
||||
// Mateix fallback que render(): a-z es resol al glif A-Z.
|
||||
if (c >= 'a' && c <= 'z') {
|
||||
c -= 32;
|
||||
}
|
||||
return chars_.contains(c);
|
||||
}
|
||||
|
||||
@@ -214,6 +227,14 @@ namespace Graphics {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Fallback de la font (només tenim glifs en majúscula): tractem
|
||||
// les minúscules a-z com les seves majúscules A-Z. Mentre no hi
|
||||
// haja glifs de minúscula, això evita que el text en minúscules
|
||||
// (p. ex. rutes de fitxer) desaparega.
|
||||
if (c >= 'a' && c <= 'z') {
|
||||
c -= 32;
|
||||
}
|
||||
|
||||
// Verificar si el carácter está soportado
|
||||
auto it = chars_.find(c);
|
||||
if (it != chars_.end()) {
|
||||
@@ -221,7 +242,8 @@ namespace Graphics {
|
||||
// Ajustar X e Y para que position represente esquina superior izquierda
|
||||
// (render_shape espera el centro, así que sumamos la mitad de ancho y altura)
|
||||
Vec2 char_pos = {.x = current_x + (CHAR_WIDTH_SCALED / 2.0F), .y = position.y + (CHAR_HEIGHT_SCALED / 2.0F)};
|
||||
Rendering::renderShape(renderer_, it->second, char_pos, 0.0F, scale, 1.0F, brightness, color);
|
||||
// Text opt-out del glow: HUD/marker s'ha de mantenir net.
|
||||
Rendering::renderShape(renderer_, it->second, char_pos, 0.0F, scale, 1.0F, brightness, color, 0.0F, 1.0F, /*glow=*/false);
|
||||
|
||||
// Avanzar posición
|
||||
current_x += CHAR_WIDTH_SCALED + SPACING_SCALED;
|
||||
@@ -234,19 +256,19 @@ namespace Graphics {
|
||||
}
|
||||
}
|
||||
|
||||
void VectorText::renderCentered(const std::string& text, const Vec2& centre_punt, float scale, float spacing, float brightness, SDL_Color color) const {
|
||||
void VectorText::renderCentered(const std::string& text, const Vec2& centre_point, float scale, float spacing, float brightness, SDL_Color color) const {
|
||||
// Calcular dimensions del text
|
||||
float text_width = getTextWidth(text, scale, spacing);
|
||||
float text_height = getTextHeight(scale);
|
||||
|
||||
// Calcular posición de l'esquina superior izquierda
|
||||
// restant la meitat de las dimensions del point central
|
||||
Vec2 posicio_esquerra = {
|
||||
.x = centre_punt.x - (text_width / 2.0F),
|
||||
.y = centre_punt.y - (text_height / 2.0F)};
|
||||
Vec2 top_left_position = {
|
||||
.x = centre_point.x - (text_width / 2.0F),
|
||||
.y = centre_point.y - (text_height / 2.0F)};
|
||||
|
||||
// Delegar al método render() existent
|
||||
render(text, posicio_esquerra, scale, spacing, brightness, color);
|
||||
render(text, top_left_position, scale, spacing, brightness, color);
|
||||
}
|
||||
|
||||
auto VectorText::getTextWidth(const std::string& text, float scale, float spacing) -> float {
|
||||
@@ -280,4 +302,8 @@ namespace Graphics {
|
||||
return BASE_CHAR_HEIGHT * scale;
|
||||
}
|
||||
|
||||
auto VectorText::getGlyphHeight(float scale) -> float {
|
||||
return BASE_GLYPH_HEIGHT * scale;
|
||||
}
|
||||
|
||||
} // namespace Graphics
|
||||
|
||||
@@ -21,22 +21,22 @@ namespace Graphics {
|
||||
|
||||
// Renderizar string completo
|
||||
// - text: cadena a renderizar (soporta: A-Z, a-z, 0-9, '.', ',', '-', ':',
|
||||
// '!', '?', ' ')
|
||||
// '!', '?', '/', '(', ')', ' ')
|
||||
// - position: posición inicial (esquina superior izquierda)
|
||||
// - scale: factor de scale (1.0 = 20×40 px por carácter)
|
||||
// - spacing: espacio entre caracteres en píxeles (a scale 1.0)
|
||||
// - brightness: factor de brightness (0.0-1.0, default 1.0 = màxima brightness)
|
||||
// - color: color RGBA explícit; si alpha==0 (default) s'usa l'oscil·lador global
|
||||
// - color: color RGBA explícit; si alpha==0 (default) es fa fallback a Rendering::DEFAULT_LINE_COLOR
|
||||
void render(const std::string& text, const Vec2& position, float scale = 1.0F, float spacing = 2.0F, float brightness = 1.0F, SDL_Color color = {0, 0, 0, 0}) const;
|
||||
|
||||
// Renderizar string centrado en un punto
|
||||
// - text: cadena a renderizar
|
||||
// - centre_punt: punto central del texto (no esquina superior izquierda)
|
||||
// - centre_point: punto central del texto (no esquina superior izquierda)
|
||||
// - scale: factor de scale (1.0 = 20×40 px por carácter)
|
||||
// - spacing: espacio entre caracteres en píxeles (a scale 1.0)
|
||||
// - brightness: factor de brightness (0.0-1.0, default 1.0 = màxima brightness)
|
||||
// - color: color RGBA explícit; si alpha==0 (default) s'usa l'oscil·lador global
|
||||
void renderCentered(const std::string& text, const Vec2& centre_punt, float scale = 1.0F, float spacing = 2.0F, float brightness = 1.0F, SDL_Color color = {0, 0, 0, 0}) const;
|
||||
// - color: color RGBA explícit; si alpha==0 (default) es fa fallback a Rendering::DEFAULT_LINE_COLOR
|
||||
void renderCentered(const std::string& text, const Vec2& centre_point, float scale = 1.0F, float spacing = 2.0F, float brightness = 1.0F, SDL_Color color = {0, 0, 0, 0}) const;
|
||||
|
||||
// Calcular ancho total de un string (útil para centrado).
|
||||
// Es estático: no depende del estado del VectorText (el ancho viene de
|
||||
@@ -46,6 +46,10 @@ namespace Graphics {
|
||||
// Calcular altura del texto (útil para centrado vertical).
|
||||
[[nodiscard]] static auto getTextHeight(float scale = 1.0F) -> float;
|
||||
|
||||
// Altura real del glif (la majúscula/dígit, sense el marge vertical de la
|
||||
// cel·la). Útil per dimensionar icones que han de casar amb el text.
|
||||
[[nodiscard]] static auto getGlyphHeight(float scale = 1.0F) -> float;
|
||||
|
||||
// Verificar si un carácter está soportado
|
||||
[[nodiscard]] auto isSupported(char c) const -> bool;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// Mesh3D = llista de vèrtexs Vec3 + llista d'arestes (parells d'índexs).
|
||||
// drawWireframe() aplica una Transform3D al mesh, projecta amb Camera3D i
|
||||
// emet cada aresta com una línia 2D pel pipeline `Rendering::linea` (mateix
|
||||
// pipeline que la resta del joc: glow verd via ColorOscillator si color.a==0).
|
||||
// pipeline que la resta del joc: verd fòsfor via Rendering::DEFAULT_LINE_COLOR si color.a==0).
|
||||
//
|
||||
// Sense depth buffer: el caller és responsable d'ordenar els meshos per
|
||||
// profunditat decreixent si vol oclusió coherent (la pipeline és LINE_LIST
|
||||
|
||||
@@ -0,0 +1,407 @@
|
||||
// define_inputs.cpp - Implementacio de l'overlay modal de redefinicio
|
||||
// © 2026 JailDesigner
|
||||
|
||||
#include "core/input/define_inputs.hpp"
|
||||
|
||||
#include <format>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "core/audio/audio.hpp"
|
||||
#include "core/defaults/service_menu.hpp"
|
||||
#include "core/input/input.hpp"
|
||||
#include "core/locale/locale.hpp"
|
||||
#include "core/types.hpp"
|
||||
#include "game/config_yaml.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr float CANVAS_W = 1280.0F;
|
||||
constexpr float CANVAS_H = 720.0F;
|
||||
|
||||
// Llindar de trigger per a edge-detect L2/R2 com a boto virtual.
|
||||
constexpr Sint16 TRIGGER_THRESHOLD = 16384;
|
||||
|
||||
// Codis virtuals per als triggers (consistents amb input_types.cpp).
|
||||
constexpr int TRIGGER_L2_VIRTUAL = 100;
|
||||
constexpr int TRIGGER_R2_VIRTUAL = 101;
|
||||
|
||||
// Durada del missatge de confirmacio abans de tancar-se.
|
||||
constexpr float COMPLETE_DISPLAY_S = 1.5F;
|
||||
|
||||
// Llindar dpad als axis sticks: no es captura per evitar conflicte amb el
|
||||
// moviment LEFT/RIGHT/UP/DOWN (que es presuposen no redefinibles al mando).
|
||||
constexpr Sint16 STICK_THRESHOLD = 16384;
|
||||
|
||||
// Crida pushRect amb un SDL_Color (les components s'escalen a [0..1]).
|
||||
void fillRect(Rendering::Renderer* renderer, float x, float y, float w, float h, SDL_Color color) {
|
||||
renderer->pushRect(x, y, w, h, static_cast<float>(color.r) / 255.0F, static_cast<float>(color.g) / 255.0F, static_cast<float>(color.b) / 255.0F, static_cast<float>(color.a) / 255.0F);
|
||||
}
|
||||
|
||||
auto titleKey(System::DefineInputs::Mode mode, System::DefineInputs::Player player) -> std::string {
|
||||
const bool IS_KB = (mode == System::DefineInputs::Mode::KEYBOARD);
|
||||
const bool IS_P1 = (player == System::DefineInputs::Player::P1);
|
||||
if (IS_KB && IS_P1) {
|
||||
return "define.title_keyboard_p1";
|
||||
}
|
||||
if (IS_KB) {
|
||||
return "define.title_keyboard_p2";
|
||||
}
|
||||
if (IS_P1) {
|
||||
return "define.title_gamepad_p1";
|
||||
}
|
||||
return "define.title_gamepad_p2";
|
||||
}
|
||||
|
||||
// Scancodes que MAI capturem com a binding (reservats per a navegacio o
|
||||
// global hotkeys). Tornen true → handleEvent les deixa passar al pipeline
|
||||
// global perque facin la seua feina (ESC obre el prompt d'eixida, F1-F12
|
||||
// son hotkeys de sistema, RETURN/BACKSPACE/TAB son navegacio).
|
||||
auto isReservedScancode(SDL_Scancode sc) -> bool {
|
||||
if (sc == SDL_SCANCODE_ESCAPE) {
|
||||
return true;
|
||||
}
|
||||
if (sc >= SDL_SCANCODE_F1 && sc <= SDL_SCANCODE_F12) {
|
||||
return true;
|
||||
}
|
||||
if (sc == SDL_SCANCODE_RETURN || sc == SDL_SCANCODE_KP_ENTER) {
|
||||
return true;
|
||||
}
|
||||
if (sc == SDL_SCANCODE_BACKSPACE || sc == SDL_SCANCODE_TAB) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Conversio sense pèrdua de SDL_Scancode → int per a comparacions
|
||||
// homogenies dins de sequence_ (que guarda codis de tots dos modes).
|
||||
auto scancodeToInt(SDL_Scancode sc) -> int {
|
||||
return static_cast<int>(sc);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace System {
|
||||
|
||||
std::unique_ptr<DefineInputs> DefineInputs::instance;
|
||||
|
||||
void DefineInputs::init(Rendering::Renderer* renderer) {
|
||||
if (!instance) {
|
||||
instance = std::unique_ptr<DefineInputs>(new DefineInputs(renderer));
|
||||
}
|
||||
}
|
||||
|
||||
void DefineInputs::destroy() { instance.reset(); }
|
||||
|
||||
auto DefineInputs::get() -> DefineInputs* { return instance.get(); }
|
||||
|
||||
DefineInputs::DefineInputs(Rendering::Renderer* renderer)
|
||||
: renderer_(renderer),
|
||||
text_(renderer) {}
|
||||
|
||||
auto DefineInputs::isActive() const -> bool {
|
||||
return phase_ != Phase::INACTIVE;
|
||||
}
|
||||
|
||||
auto DefineInputs::begin(Mode mode, Player player) -> bool {
|
||||
if (mode == Mode::GAMEPAD) {
|
||||
// Requereix un pad assignat al jugador.
|
||||
const auto* input = Input::get();
|
||||
if (input == nullptr) {
|
||||
return false;
|
||||
}
|
||||
const int IDX = (player == Player::P1) ? 0 : 1;
|
||||
if (input->getPlayerGamepad(IDX) == nullptr) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
mode_ = mode;
|
||||
player_ = player;
|
||||
index_ = 0;
|
||||
complete_timer_s_ = 0.0F;
|
||||
l2_was_pressed_ = false;
|
||||
r2_was_pressed_ = false;
|
||||
|
||||
buildSequence();
|
||||
phase_ = Phase::CAPTURING;
|
||||
return true;
|
||||
}
|
||||
|
||||
void DefineInputs::cancel() {
|
||||
phase_ = Phase::INACTIVE;
|
||||
sequence_.clear();
|
||||
index_ = 0;
|
||||
complete_timer_s_ = 0.0F;
|
||||
}
|
||||
|
||||
void DefineInputs::buildSequence() {
|
||||
sequence_.clear();
|
||||
if (mode_ == Mode::KEYBOARD) {
|
||||
// Teclat: LEFT, RIGHT, FIRE (SHOOT), ACCELERATE (THRUST)
|
||||
sequence_.push_back({.action_label_key = "define.action.left", .action = InputAction::LEFT, .captured = -1});
|
||||
sequence_.push_back({.action_label_key = "define.action.right", .action = InputAction::RIGHT, .captured = -1});
|
||||
sequence_.push_back({.action_label_key = "define.action.fire", .action = InputAction::SHOOT, .captured = -1});
|
||||
sequence_.push_back({.action_label_key = "define.action.accelerate", .action = InputAction::THRUST, .captured = -1});
|
||||
} else {
|
||||
// Mando: FIRE, ACCELERATE, START, MENU
|
||||
sequence_.push_back({.action_label_key = "define.action.fire", .action = InputAction::SHOOT, .captured = -1});
|
||||
sequence_.push_back({.action_label_key = "define.action.accelerate", .action = InputAction::THRUST, .captured = -1});
|
||||
sequence_.push_back({.action_label_key = "define.action.start", .action = InputAction::START, .captured = -1});
|
||||
sequence_.push_back({.action_label_key = "define.action.menu", .action = InputAction::MENU, .captured = -1});
|
||||
}
|
||||
}
|
||||
|
||||
auto DefineInputs::isInUse(int code) const -> bool {
|
||||
for (const auto& s : sequence_) {
|
||||
if (s.captured == code) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void DefineInputs::captureAndAdvance(int code) {
|
||||
if (index_ >= sequence_.size()) {
|
||||
return;
|
||||
}
|
||||
if (isInUse(code)) {
|
||||
return; // Duplicat dins de la sessio: rebutgem silenciosament
|
||||
}
|
||||
sequence_[index_].captured = code;
|
||||
++index_;
|
||||
if (auto* audio = Audio::get(); audio != nullptr) {
|
||||
audio->playSound(Defaults::ServiceMenu::ACCEPT_SOUND, Audio::Group::INTERFACE);
|
||||
}
|
||||
if (index_ >= sequence_.size()) {
|
||||
persistAndComplete();
|
||||
}
|
||||
}
|
||||
|
||||
void DefineInputs::persistAndComplete() {
|
||||
auto& cfg = (player_ == Player::P1)
|
||||
? ConfigYaml::engine_config.player1
|
||||
: ConfigYaml::engine_config.player2;
|
||||
|
||||
if (mode_ == Mode::KEYBOARD) {
|
||||
for (const Step& s : sequence_) {
|
||||
switch (s.action) {
|
||||
case InputAction::LEFT:
|
||||
cfg.keyboard.key_left = static_cast<SDL_Scancode>(s.captured);
|
||||
break;
|
||||
case InputAction::RIGHT:
|
||||
cfg.keyboard.key_right = static_cast<SDL_Scancode>(s.captured);
|
||||
break;
|
||||
case InputAction::SHOOT:
|
||||
cfg.keyboard.key_shoot = static_cast<SDL_Scancode>(s.captured);
|
||||
break;
|
||||
case InputAction::THRUST:
|
||||
cfg.keyboard.key_thrust = static_cast<SDL_Scancode>(s.captured);
|
||||
break;
|
||||
default:
|
||||
break; // START / MENU no es redefineixen al teclat
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const Step& s : sequence_) {
|
||||
switch (s.action) {
|
||||
case InputAction::SHOOT:
|
||||
cfg.gamepad.button_shoot = s.captured;
|
||||
break;
|
||||
case InputAction::THRUST:
|
||||
cfg.gamepad.button_thrust = s.captured;
|
||||
break;
|
||||
case InputAction::START:
|
||||
cfg.gamepad.button_start = s.captured;
|
||||
break;
|
||||
case InputAction::MENU:
|
||||
cfg.gamepad.button_menu = s.captured;
|
||||
break;
|
||||
default:
|
||||
break; // LEFT / RIGHT no es redefineixen al mando
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Aplicar canvis al runtime de l'Input i persistir a disc.
|
||||
if (auto* input = Input::get(); input != nullptr) {
|
||||
if (player_ == Player::P1) {
|
||||
input->applyPlayer1Bindings(ConfigYaml::engine_config.player1);
|
||||
} else {
|
||||
input->applyPlayer2Bindings(ConfigYaml::engine_config.player2);
|
||||
}
|
||||
}
|
||||
ConfigYaml::saveToFile();
|
||||
|
||||
phase_ = Phase::COMPLETE;
|
||||
complete_timer_s_ = COMPLETE_DISPLAY_S;
|
||||
}
|
||||
|
||||
void DefineInputs::update(float delta_time) {
|
||||
if (phase_ != Phase::COMPLETE) {
|
||||
return;
|
||||
}
|
||||
complete_timer_s_ -= delta_time;
|
||||
if (complete_timer_s_ <= 0.0F) {
|
||||
cancel();
|
||||
}
|
||||
}
|
||||
|
||||
void DefineInputs::processTrigger(int virtual_button, bool& was_pressed, bool now) {
|
||||
if (now && !was_pressed) {
|
||||
captureAndAdvance(virtual_button);
|
||||
}
|
||||
was_pressed = now;
|
||||
}
|
||||
|
||||
auto DefineInputs::handleKeyboardEvent(const SDL_Event& event) -> bool {
|
||||
if (event.type != SDL_EVENT_KEY_DOWN) {
|
||||
return true; // Empassem la resta sense fer res
|
||||
}
|
||||
const SDL_Scancode SC = event.key.scancode;
|
||||
if (isReservedScancode(SC)) {
|
||||
// ESC, F1-F12, RETURN, BACKSPACE, TAB es deixen passar al pipeline
|
||||
// global (ESC obre el prompt d'eixida; F1-F12 hotkeys, etc.).
|
||||
return false;
|
||||
}
|
||||
captureAndAdvance(scancodeToInt(SC));
|
||||
return true;
|
||||
}
|
||||
|
||||
auto DefineInputs::handleGamepadEvent(const SDL_Event& event) -> bool {
|
||||
// KEY_DOWN no es per al rebind de mando: deixem que el global el
|
||||
// gestioni (ex. ESC → prompt d'eixida, F12 → tanca menu, etc.).
|
||||
if (event.type == SDL_EVENT_KEY_DOWN) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Filtrar events al pad del jugador actiu.
|
||||
const auto* input = Input::get();
|
||||
if (input == nullptr) {
|
||||
return true;
|
||||
}
|
||||
const int IDX = (player_ == Player::P1) ? 0 : 1;
|
||||
auto pad = input->getPlayerGamepad(IDX);
|
||||
if (!pad) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) {
|
||||
if (event.gbutton.which != pad->instance_id) {
|
||||
return true;
|
||||
}
|
||||
captureAndAdvance(static_cast<int>(event.gbutton.button));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.type == SDL_EVENT_GAMEPAD_AXIS_MOTION) {
|
||||
if (event.gaxis.which != pad->instance_id) {
|
||||
return true;
|
||||
}
|
||||
const auto AXIS = static_cast<SDL_GamepadAxis>(event.gaxis.axis);
|
||||
const Sint16 VAL = event.gaxis.value;
|
||||
if (AXIS == SDL_GAMEPAD_AXIS_LEFT_TRIGGER) {
|
||||
processTrigger(TRIGGER_L2_VIRTUAL, l2_was_pressed_, VAL >= TRIGGER_THRESHOLD);
|
||||
} else if (AXIS == SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) {
|
||||
processTrigger(TRIGGER_R2_VIRTUAL, r2_was_pressed_, VAL >= TRIGGER_THRESHOLD);
|
||||
}
|
||||
// Sticks LEFTX/LEFTY/RIGHTX/RIGHTY: ignorats (no son redefinibles).
|
||||
(void)STICK_THRESHOLD;
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
auto DefineInputs::handleEvent(const SDL_Event& event) -> bool {
|
||||
if (phase_ == Phase::INACTIVE) {
|
||||
return false;
|
||||
}
|
||||
// SDL_EVENT_QUIT i WINDOW_CLOSE_REQUESTED han de poder tancar la
|
||||
// finestra encara que el modal estiga obert; els passem al pipeline.
|
||||
if (event.type == SDL_EVENT_QUIT ||
|
||||
event.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED) {
|
||||
return false;
|
||||
}
|
||||
if (phase_ == Phase::COMPLETE) {
|
||||
// Mentre mostrem el missatge OK, empassem la resta d'events sense
|
||||
// capturar perque l'usuari no puga avançar accions sense voler.
|
||||
return true;
|
||||
}
|
||||
if (mode_ == Mode::KEYBOARD) {
|
||||
return handleKeyboardEvent(event);
|
||||
}
|
||||
return handleGamepadEvent(event);
|
||||
}
|
||||
|
||||
void DefineInputs::draw() const {
|
||||
if (phase_ == Phase::INACTIVE) {
|
||||
return;
|
||||
}
|
||||
|
||||
using namespace Defaults::ServiceMenu;
|
||||
|
||||
// Caixa centrada, dimensions fixes (no depen del contingut a redefinir).
|
||||
constexpr float BOX_W = 560.0F;
|
||||
constexpr float BOX_H = 280.0F;
|
||||
const float BOX_X = (CANVAS_W - BOX_W) * 0.5F;
|
||||
const float BOX_Y = (CANVAS_H - BOX_H) * 0.5F;
|
||||
|
||||
// Fons + brackets als 4 cantons (estil HUD del menu de servei).
|
||||
fillRect(renderer_, BOX_X, BOX_Y, BOX_W, BOX_H, BG_COLOR);
|
||||
|
||||
const auto T = static_cast<float>(CORNER_THICKNESS);
|
||||
const auto AH = static_cast<float>(CORNER_ARM_H);
|
||||
const auto AV = static_cast<float>(CORNER_ARM_V);
|
||||
fillRect(renderer_, BOX_X, BOX_Y, AH, T, CORNER_COLOR);
|
||||
fillRect(renderer_, BOX_X, BOX_Y, T, AV, CORNER_COLOR);
|
||||
fillRect(renderer_, BOX_X + BOX_W - AH, BOX_Y, AH, T, CORNER_COLOR);
|
||||
fillRect(renderer_, BOX_X + BOX_W - T, BOX_Y, T, AV, CORNER_COLOR);
|
||||
fillRect(renderer_, BOX_X, BOX_Y + BOX_H - T, AH, T, CORNER_COLOR);
|
||||
fillRect(renderer_, BOX_X, BOX_Y + BOX_H - AV, T, AV, CORNER_COLOR);
|
||||
fillRect(renderer_, BOX_X + BOX_W - AH, BOX_Y + BOX_H - T, AH, T, CORNER_COLOR);
|
||||
fillRect(renderer_, BOX_X + BOX_W - T, BOX_Y + BOX_H - AV, T, AV, CORNER_COLOR);
|
||||
|
||||
const std::string TITLE = Locale::get().text(titleKey(mode_, player_));
|
||||
const float TITLE_W = Graphics::VectorText::getTextWidth(TITLE, TITLE_SCALE, TEXT_SPACING);
|
||||
const float TITLE_X = BOX_X + ((BOX_W - TITLE_W) * 0.5F);
|
||||
const float TITLE_Y = BOX_Y + 26.0F;
|
||||
text_.render(TITLE, Vec2{.x = TITLE_X, .y = TITLE_Y}, TITLE_SCALE, TEXT_SPACING, 1.0F, TITLE_COLOR);
|
||||
|
||||
if (phase_ == Phase::COMPLETE) {
|
||||
const std::string OK = Locale::get().text("define.complete");
|
||||
constexpr float OK_SCALE = 0.7F;
|
||||
const float OK_W = Graphics::VectorText::getTextWidth(OK, OK_SCALE, TEXT_SPACING);
|
||||
const float OK_X = BOX_X + ((BOX_W - OK_W) * 0.5F);
|
||||
const float OK_Y = BOX_Y + (BOX_H * 0.5F) - 10.0F;
|
||||
constexpr SDL_Color OK_COLOR{.r = 120, .g = 255, .b = 140, .a = 255};
|
||||
text_.render(OK, Vec2{.x = OK_X, .y = OK_Y}, OK_SCALE, TEXT_SPACING, 1.0F, OK_COLOR);
|
||||
return;
|
||||
}
|
||||
|
||||
// Instruccio (premeu tecla / boto) + accio actual + progres.
|
||||
const std::string PROMPT = Locale::get().text(
|
||||
mode_ == Mode::KEYBOARD ? "define.press_key" : "define.press_button");
|
||||
const float PROMPT_W = Graphics::VectorText::getTextWidth(PROMPT, ITEM_SCALE, TEXT_SPACING);
|
||||
const float PROMPT_X = BOX_X + ((BOX_W - PROMPT_W) * 0.5F);
|
||||
const float PROMPT_Y = BOX_Y + 86.0F;
|
||||
text_.render(PROMPT, Vec2{.x = PROMPT_X, .y = PROMPT_Y}, ITEM_SCALE, TEXT_SPACING, 1.0F, SUBTITLE_COLOR);
|
||||
|
||||
if (index_ < sequence_.size()) {
|
||||
const std::string ACTION = Locale::get().text(sequence_[index_].action_label_key);
|
||||
constexpr float ACTION_SCALE = 0.9F;
|
||||
const float ACTION_W = Graphics::VectorText::getTextWidth(ACTION, ACTION_SCALE, TEXT_SPACING);
|
||||
const float ACTION_X = BOX_X + ((BOX_W - ACTION_W) * 0.5F);
|
||||
const float ACTION_Y = BOX_Y + 130.0F;
|
||||
text_.render(ACTION, Vec2{.x = ACTION_X, .y = ACTION_Y}, ACTION_SCALE, TEXT_SPACING, 1.0F, CURSOR_COLOR);
|
||||
}
|
||||
|
||||
const std::string PROGRESS = std::format("{}/{}", index_ + 1, sequence_.size());
|
||||
constexpr float PROG_SCALE = 0.4F;
|
||||
const float PROG_W = Graphics::VectorText::getTextWidth(PROGRESS, PROG_SCALE, TEXT_SPACING);
|
||||
const float PROG_X = BOX_X + ((BOX_W - PROG_W) * 0.5F);
|
||||
const float PROG_Y = BOX_Y + 200.0F;
|
||||
text_.render(PROGRESS, Vec2{.x = PROG_X, .y = PROG_Y}, PROG_SCALE, TEXT_SPACING, 1.0F, LABEL_COLOR);
|
||||
}
|
||||
|
||||
} // namespace System
|
||||
@@ -0,0 +1,107 @@
|
||||
// define_inputs.hpp - Overlay modal de redefinici de controls (singleton)
|
||||
// © 2026 JailDesigner
|
||||
//
|
||||
// Sub-mòdul inspirat en aee_arcade/source/core/input/define_buttons. Quan el
|
||||
// menú de servei dispara una acció "Redefinir tecles/botons P1/P2", aquest
|
||||
// singleton pren el control: pinta una caixa central, captura events SDL i
|
||||
// avança per una seqüència fixa d'accions, persistint les noves assignacions
|
||||
// a config.yaml en acabar.
|
||||
//
|
||||
// Cicle de vida:
|
||||
// 1. begin(mode, player) → construeix la seqüència (4 passos) i activa
|
||||
// l'overlay. Per a GAMEPAD, retorna false si el jugador no té pad.
|
||||
// 2. handleEvent() captura el següent event vàlid; ESC cancel·la sense
|
||||
// desar; duplicats dins de la sessió es rebutgen silenciosament.
|
||||
// 3. Quan la seqüència es completa, persistim a engine_config + saveToFile,
|
||||
// reapliquem els bindings i mostrem un missatge "OK" durant 1.5 s
|
||||
// abans d'auto-tancar-se.
|
||||
//
|
||||
// El routing d'events es fa des de GlobalEvents::handle: mentre isActive()
|
||||
// retorna true, tots els events SDL es desvien aquí i no arriben al joc ni
|
||||
// al menú de servei.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "core/graphics/vector_text.hpp"
|
||||
#include "core/input/input_types.hpp"
|
||||
#include "core/rendering/render_context.hpp"
|
||||
|
||||
namespace System {
|
||||
|
||||
class DefineInputs {
|
||||
public:
|
||||
enum class Mode : std::uint8_t { KEYBOARD,
|
||||
GAMEPAD };
|
||||
enum class Player : std::uint8_t { P1,
|
||||
P2 };
|
||||
|
||||
static void init(Rendering::Renderer* renderer);
|
||||
static void destroy();
|
||||
[[nodiscard]] static auto get() -> DefineInputs*;
|
||||
|
||||
// Comença la sessió. Retorna false per a GAMEPAD si el jugador no té
|
||||
// cap pad assignat (el caller hauria de notificar a l'usuari abans).
|
||||
auto begin(Mode mode, Player player) -> bool;
|
||||
void cancel();
|
||||
|
||||
[[nodiscard]] auto isActive() const -> bool;
|
||||
|
||||
void update(float delta_time);
|
||||
void draw() const;
|
||||
|
||||
// Retorna true si l'event s'ha consumit (és a dir, mentre l'overlay
|
||||
// és actiu sempre consumeix tot per evitar passages al joc o menú).
|
||||
auto handleEvent(const SDL_Event& event) -> bool;
|
||||
|
||||
private:
|
||||
explicit DefineInputs(Rendering::Renderer* renderer);
|
||||
|
||||
enum class Phase : std::uint8_t {
|
||||
INACTIVE,
|
||||
CAPTURING,
|
||||
COMPLETE, // mostra missatge OK breu abans d'auto-cancel
|
||||
};
|
||||
|
||||
struct Step {
|
||||
std::string action_label_key; // p.ex. "define.action.left"
|
||||
InputAction action; // mapeig a la struct PlayerBindings
|
||||
int captured{-1}; // scancode o button code; -1 = sense capturar
|
||||
};
|
||||
|
||||
void buildSequence();
|
||||
[[nodiscard]] auto isInUse(int code) const -> bool;
|
||||
void captureAndAdvance(int code);
|
||||
void persistAndComplete();
|
||||
|
||||
// Handlers especialitzats segons mode_.
|
||||
auto handleKeyboardEvent(const SDL_Event& event) -> bool;
|
||||
auto handleGamepadEvent(const SDL_Event& event) -> bool;
|
||||
|
||||
// Edge-detect per als triggers L2/R2 com a botons virtuals.
|
||||
void processTrigger(int virtual_button, bool& was_pressed, bool now);
|
||||
|
||||
Rendering::Renderer* renderer_;
|
||||
Graphics::VectorText text_;
|
||||
|
||||
Phase phase_{Phase::INACTIVE};
|
||||
Mode mode_{Mode::KEYBOARD};
|
||||
Player player_{Player::P1};
|
||||
std::vector<Step> sequence_;
|
||||
std::size_t index_{0};
|
||||
float complete_timer_s_{0.0F};
|
||||
|
||||
// Estat d'edge-detect dels triggers durant la sessió GAMEPAD.
|
||||
bool l2_was_pressed_{false};
|
||||
bool r2_was_pressed_{false};
|
||||
|
||||
static std::unique_ptr<DefineInputs> instance;
|
||||
};
|
||||
|
||||
} // namespace System
|
||||
+103
-29
@@ -2,12 +2,15 @@
|
||||
|
||||
#include <SDL3/SDL.h> // Para SDL_GetGamepadAxis, SDL_GamepadAxis, SDL_GamepadButton, SDL_GetError, SDL_JoystickID, SDL_AddGamepadMappingsFromFile, SDL_Event, SDL_EventType, SDL_GetGamepadButton, SDL_GetKeyboardState, SDL_INIT_GAMEPAD, SDL_InitSubSystem, SDL_LogError, SDL_OpenGamepad, SDL_PollEvent, SDL_WasInit, Sint16, SDL_Gamepad, SDL_LogCategory, SDL_Scancode
|
||||
|
||||
#include <algorithm> // Para std::ranges::any_of
|
||||
#include <iostream> // Para basic_ostream, operator<<, cout, cerr
|
||||
#include <memory> // Para shared_ptr, __shared_ptr_access, allocator, operator==, make_shared
|
||||
#include <unordered_map> // Para unordered_map, _Node_iterator, operator==, _Node_iterator_base, _Node_const_iterator
|
||||
#include <utility> // Para move
|
||||
|
||||
#include "core/locale/locale.hpp"
|
||||
#include "core/system/notifier.hpp"
|
||||
#include "core/utils/string_utils.hpp"
|
||||
|
||||
// Singleton
|
||||
Input* Input::instance = nullptr;
|
||||
|
||||
@@ -162,9 +165,12 @@ auto Input::checkAnyButton(bool repeat) -> bool {
|
||||
|
||||
// Comprueba si algún player (P1 o P2) presionó alguna acción de una lista
|
||||
auto Input::checkAnyPlayerAction(const std::span<const InputAction>& actions, bool repeat) -> bool {
|
||||
return std::ranges::any_of(actions, [this, repeat](const InputAction& action) {
|
||||
return checkActionPlayer1(action, repeat) || checkActionPlayer2(action, repeat);
|
||||
});
|
||||
for (const auto& action : actions) {
|
||||
if (checkActionPlayer1(action, repeat) || checkActionPlayer2(action, repeat)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Comprueba si hay algun mando conectado
|
||||
@@ -303,8 +309,11 @@ auto Input::checkTriggerInput(Action action, const std::shared_ptr<Gamepad>& gam
|
||||
}
|
||||
|
||||
void Input::addGamepadMappingsFromFile() {
|
||||
if (SDL_AddGamepadMappingsFromFile(gamepad_mappings_file_.c_str()) < 0) {
|
||||
std::cout << "Error, could not load " << gamepad_mappings_file_.c_str() << " file: " << SDL_GetError() << '\n';
|
||||
const int COUNT = SDL_AddGamepadMappingsFromFile(gamepad_mappings_file_.c_str());
|
||||
if (COUNT < 0) {
|
||||
std::cerr << "[Input] Error carregant " << gamepad_mappings_file_ << ": " << SDL_GetError() << '\n';
|
||||
} else {
|
||||
std::cout << "[Input] " << gamepad_mappings_file_ << " carregat (" << COUNT << " mappings)\n";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -322,8 +331,7 @@ void Input::initSDLGamePad() {
|
||||
} else {
|
||||
addGamepadMappingsFromFile();
|
||||
discoverGamepads();
|
||||
std::cout << "\n** INPUT SYSTEM **\n";
|
||||
std::cout << "Input System initialized successfully\n";
|
||||
std::cout << "[Input] inicialitzat\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -373,9 +381,25 @@ void Input::update() {
|
||||
|
||||
// --- MANDOS ---
|
||||
for (const auto& gamepad : gamepads_) {
|
||||
// LEFT i RIGHT NO son redefinibles al mando (assumits dpad o stick).
|
||||
// Llegim el left stick X i el fusionem amb l'estat del dpad: qualsevol
|
||||
// de les dos fonts activa l'accio. Llindar AXIS_THRESHOLD (30000).
|
||||
const Sint16 STICK_X = SDL_GetGamepadAxis(gamepad->pad, SDL_GAMEPAD_AXIS_LEFTX);
|
||||
const bool STICK_LEFT = STICK_X < -AXIS_THRESHOLD;
|
||||
const bool STICK_RIGHT = STICK_X > AXIS_THRESHOLD;
|
||||
|
||||
for (auto& binding : gamepad->bindings) {
|
||||
bool button_is_down_now = static_cast<int>(SDL_GetGamepadButton(gamepad->pad, static_cast<SDL_GamepadButton>(binding.second.button))) != 0;
|
||||
|
||||
// Per a LEFT/RIGHT, fer un OR amb el stick X. La resta d'accions
|
||||
// (THRUST/SHOOT/START/MENU) ignoren el stick aqui — si es vol
|
||||
// dispar amb trigger L2/R2 cal binding amb codi 100/101.
|
||||
if (binding.first == Action::LEFT) {
|
||||
button_is_down_now = button_is_down_now || STICK_LEFT;
|
||||
} else if (binding.first == Action::RIGHT) {
|
||||
button_is_down_now = button_is_down_now || STICK_RIGHT;
|
||||
}
|
||||
|
||||
// El estado .is_held del fotograma anterior nos sirve para saber si es un pulso nuevo
|
||||
binding.second.just_pressed = button_is_down_now && !binding.second.is_held;
|
||||
binding.second.is_held = button_is_down_now;
|
||||
@@ -407,18 +431,40 @@ auto Input::addGamepad(int device_index) -> std::string {
|
||||
auto name = gamepad->name;
|
||||
std::cout << "Gamepad connected (" << name << ")" << '\n';
|
||||
gamepads_.push_back(std::move(gamepad));
|
||||
|
||||
// Toast a pantalla. Pot ser nullptr durant discoverGamepads() inicial
|
||||
// (l'Input::init() es crida abans que el Director instanciï el Notifier).
|
||||
if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
|
||||
notifier->notifyInfo(localeSubstitute(
|
||||
Locale::get().text("notification.gamepad_connected"),
|
||||
"{name}",
|
||||
Utils::toUpperAscii(name)));
|
||||
}
|
||||
|
||||
return name + " CONNECTED";
|
||||
}
|
||||
|
||||
auto Input::removeGamepad(SDL_JoystickID id) -> std::string {
|
||||
auto it = std::ranges::find_if(gamepads_, [id](const std::shared_ptr<Gamepad>& gamepad) {
|
||||
return gamepad->instance_id == id;
|
||||
});
|
||||
auto it = gamepads_.end();
|
||||
for (auto i = gamepads_.begin(); i != gamepads_.end(); ++i) {
|
||||
if ((*i)->instance_id == id) {
|
||||
it = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (it != gamepads_.end()) {
|
||||
std::string name = (*it)->name;
|
||||
std::cout << "Gamepad disconnected (" << name << ")" << '\n';
|
||||
gamepads_.erase(it);
|
||||
|
||||
if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
|
||||
notifier->notifyInfo(localeSubstitute(
|
||||
Locale::get().text("notification.gamepad_disconnected"),
|
||||
"{name}",
|
||||
Utils::toUpperAscii(name)));
|
||||
}
|
||||
|
||||
return name + " DISCONNECTED";
|
||||
}
|
||||
std::cerr << "No se encontró el gamepad con ID " << id << '\n';
|
||||
@@ -465,6 +511,33 @@ auto Input::findAvailableGamepadByName(const std::string& gamepad_name) -> std::
|
||||
|
||||
// ========== MÉTODOS ESPECÍFICOS POR JUGADOR (ORNI) ==========
|
||||
|
||||
// Cerca el gamepad assignat a un jugador. Prioritat path > name. Si els
|
||||
// dos camps venen buits o no n'hi ha cap match retornem nullptr (sense
|
||||
// mando explicit). L'autoassignacio inicial es resol al boot.
|
||||
auto Input::resolvePlayerGamepad(const Config::PlayerBindings& bindings) -> std::shared_ptr<Gamepad> {
|
||||
if (gamepads_.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (!bindings.gamepad_path.empty()) {
|
||||
for (const auto& pad : gamepads_) {
|
||||
if (pad && pad->path == bindings.gamepad_path) {
|
||||
return pad;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!bindings.gamepad_name.empty()) {
|
||||
for (const auto& pad : gamepads_) {
|
||||
if (pad && pad->name == bindings.gamepad_name) {
|
||||
return pad;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Aplica configuración de controles del player 1
|
||||
void Input::applyPlayer1Bindings(const Config::PlayerBindings& bindings) {
|
||||
// 1. Aplicar bindings de teclado (NO usar bindKey, llenar mapa específico)
|
||||
@@ -474,15 +547,8 @@ void Input::applyPlayer1Bindings(const Config::PlayerBindings& bindings) {
|
||||
player1_keyboard_bindings_[Action::SHOOT].scancode = bindings.keyboard.key_shoot;
|
||||
player1_keyboard_bindings_[Action::START].scancode = bindings.keyboard.key_start;
|
||||
|
||||
// 2. Encontrar gamepad por nombre (o usar primer gamepad como fallback)
|
||||
std::shared_ptr<Gamepad> gamepad = nullptr;
|
||||
if (bindings.gamepad_name.empty()) {
|
||||
// Fallback: usar primer gamepad disponible
|
||||
gamepad = (!gamepads_.empty()) ? gamepads_[0] : nullptr;
|
||||
} else {
|
||||
// Buscar por nombre
|
||||
gamepad = findAvailableGamepadByName(bindings.gamepad_name);
|
||||
}
|
||||
// 2. Resoldre gamepad per path/name
|
||||
std::shared_ptr<Gamepad> gamepad = resolvePlayerGamepad(bindings);
|
||||
|
||||
if (!gamepad) {
|
||||
player1_gamepad_ = nullptr;
|
||||
@@ -494,6 +560,8 @@ void Input::applyPlayer1Bindings(const Config::PlayerBindings& bindings) {
|
||||
gamepad->bindings[Action::RIGHT].button = bindings.gamepad.button_right;
|
||||
gamepad->bindings[Action::THRUST].button = bindings.gamepad.button_thrust;
|
||||
gamepad->bindings[Action::SHOOT].button = bindings.gamepad.button_shoot;
|
||||
gamepad->bindings[Action::START].button = bindings.gamepad.button_start;
|
||||
gamepad->bindings[Action::MENU].button = bindings.gamepad.button_menu;
|
||||
|
||||
// 4. Cachear referencia
|
||||
player1_gamepad_ = gamepad;
|
||||
@@ -508,15 +576,8 @@ void Input::applyPlayer2Bindings(const Config::PlayerBindings& bindings) {
|
||||
player2_keyboard_bindings_[Action::SHOOT].scancode = bindings.keyboard.key_shoot;
|
||||
player2_keyboard_bindings_[Action::START].scancode = bindings.keyboard.key_start;
|
||||
|
||||
// 2. Encontrar gamepad por nombre (o usar segundo gamepad como fallback)
|
||||
std::shared_ptr<Gamepad> gamepad = nullptr;
|
||||
if (bindings.gamepad_name.empty()) {
|
||||
// Fallback: usar segundo gamepad disponible
|
||||
gamepad = (gamepads_.size() > 1) ? gamepads_[1] : nullptr;
|
||||
} else {
|
||||
// Buscar por nombre
|
||||
gamepad = findAvailableGamepadByName(bindings.gamepad_name);
|
||||
}
|
||||
// 2. Resoldre gamepad per path/name
|
||||
std::shared_ptr<Gamepad> gamepad = resolvePlayerGamepad(bindings);
|
||||
|
||||
if (!gamepad) {
|
||||
player2_gamepad_ = nullptr;
|
||||
@@ -528,6 +589,8 @@ void Input::applyPlayer2Bindings(const Config::PlayerBindings& bindings) {
|
||||
gamepad->bindings[Action::RIGHT].button = bindings.gamepad.button_right;
|
||||
gamepad->bindings[Action::THRUST].button = bindings.gamepad.button_thrust;
|
||||
gamepad->bindings[Action::SHOOT].button = bindings.gamepad.button_shoot;
|
||||
gamepad->bindings[Action::START].button = bindings.gamepad.button_start;
|
||||
gamepad->bindings[Action::MENU].button = bindings.gamepad.button_menu;
|
||||
|
||||
// 4. Cachear referencia
|
||||
player2_gamepad_ = gamepad;
|
||||
@@ -555,6 +618,17 @@ auto Input::checkActionPlayer1(Action action, bool repeat) -> bool {
|
||||
return keyboard_active || gamepad_active;
|
||||
}
|
||||
|
||||
// Retorna el pad assignat (0=P1, 1=P2). Pot ser nullptr.
|
||||
auto Input::getPlayerGamepad(int player_index) const -> std::shared_ptr<Input::Gamepad> {
|
||||
if (player_index == 0) {
|
||||
return player1_gamepad_;
|
||||
}
|
||||
if (player_index == 1) {
|
||||
return player2_gamepad_;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Consulta de input para player 2
|
||||
auto Input::checkActionPlayer2(Action action, bool repeat) -> bool {
|
||||
// Comprobar teclado con el mapa específico de P2
|
||||
|
||||
@@ -62,7 +62,9 @@ class Input {
|
||||
{Action::LEFT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_DPAD_LEFT)}},
|
||||
{Action::RIGHT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_DPAD_RIGHT)}},
|
||||
{Action::THRUST, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_WEST)}},
|
||||
{Action::SHOOT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_SOUTH)}}} {}
|
||||
{Action::SHOOT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_SOUTH)}},
|
||||
{Action::START, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_START)}},
|
||||
{Action::MENU, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_BACK)}}} {}
|
||||
|
||||
~Gamepad() {
|
||||
if (pad != nullptr) {
|
||||
@@ -107,6 +109,10 @@ class Input {
|
||||
auto checkActionPlayer1(Action action, bool repeat = true) -> bool;
|
||||
auto checkActionPlayer2(Action action, bool repeat = true) -> bool;
|
||||
|
||||
// Accés al gamepad assignat per jugador (0=P1, 1=P2). nullptr si no n'hi
|
||||
// ha cap d'assignat o connectat. Usat per la UI de redefinició de botons.
|
||||
[[nodiscard]] auto getPlayerGamepad(int player_index) const -> std::shared_ptr<Gamepad>;
|
||||
|
||||
// Check if any player pressed any action from a list
|
||||
auto checkAnyPlayerAction(const std::span<const InputAction>& actions, bool repeat = DO_NOT_ALLOW_REPEAT) -> bool;
|
||||
|
||||
@@ -142,6 +148,7 @@ class Input {
|
||||
auto removeGamepad(SDL_JoystickID id) -> std::string;
|
||||
void addGamepadMappingsFromFile();
|
||||
void discoverGamepads();
|
||||
auto resolvePlayerGamepad(const Config::PlayerBindings& bindings) -> std::shared_ptr<Gamepad>;
|
||||
|
||||
// --- Variables miembro ---
|
||||
static Input* instance; // Instancia única del singleton
|
||||
|
||||
@@ -6,6 +6,8 @@ const std::unordered_map<InputAction, std::string> ACTION_TO_STRING = {
|
||||
{InputAction::RIGHT, "RIGHT"},
|
||||
{InputAction::THRUST, "THRUST"},
|
||||
{InputAction::SHOOT, "SHOOT"},
|
||||
{InputAction::START, "START"},
|
||||
{InputAction::MENU, "MENU"},
|
||||
{InputAction::WINDOW_INC_ZOOM, "WINDOW_INC_ZOOM"},
|
||||
{InputAction::WINDOW_DEC_ZOOM, "WINDOW_DEC_ZOOM"},
|
||||
{InputAction::TOGGLE_FULLSCREEN, "TOGGLE_FULLSCREEN"},
|
||||
@@ -18,6 +20,8 @@ const std::unordered_map<std::string, InputAction> STRING_TO_ACTION = {
|
||||
{"RIGHT", InputAction::RIGHT},
|
||||
{"THRUST", InputAction::THRUST},
|
||||
{"SHOOT", InputAction::SHOOT},
|
||||
{"START", InputAction::START},
|
||||
{"MENU", InputAction::MENU},
|
||||
{"WINDOW_INC_ZOOM", InputAction::WINDOW_INC_ZOOM},
|
||||
{"WINDOW_DEC_ZOOM", InputAction::WINDOW_DEC_ZOOM},
|
||||
{"TOGGLE_FULLSCREEN", InputAction::TOGGLE_FULLSCREEN},
|
||||
|
||||
@@ -15,6 +15,7 @@ enum class InputAction : std::uint8_t { // Acciones de entrada posibles en el j
|
||||
THRUST, // Acelerar
|
||||
SHOOT, // Disparar
|
||||
START, // Empezar match
|
||||
MENU, // Abrir/cerrar menu de servicio (equivalent a F12)
|
||||
|
||||
// Inputs de sistema (globales)
|
||||
WINDOW_INC_ZOOM, // F2
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
// locale.cpp - Implementació del sistema de locale
|
||||
// © 2026 JailDesigner
|
||||
|
||||
#include "core/locale/locale.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "core/resources/resource_helper.hpp"
|
||||
#include "external/fkyaml_node.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
// Recorre el node YAML i aplana jerarquies en claus "a.b.c". Suporta
|
||||
// mappings (recursió) i seqüències de strings (desa "a.b.0", "a.b.1"...).
|
||||
// Altres tipus (nombres, booleans solts) s'ignoren silenciosament.
|
||||
void flatten(const fkyaml::node& node, const std::string& prefix, std::unordered_map<std::string, std::string>& out) {
|
||||
if (node.is_mapping()) {
|
||||
for (auto it = node.begin(); it != node.end(); ++it) {
|
||||
const std::string KEY = prefix.empty()
|
||||
? it.key().get_value<std::string>()
|
||||
: prefix + "." + it.key().get_value<std::string>();
|
||||
flatten(it.value(), KEY, out);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (node.is_sequence()) {
|
||||
std::size_t index = 0;
|
||||
for (const auto& item : node) {
|
||||
const std::string KEY = prefix + "." + std::to_string(index);
|
||||
flatten(item, KEY, out);
|
||||
index++;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (node.is_string()) {
|
||||
out[prefix] = node.get_value<std::string>();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
auto Locale::get() -> Locale& {
|
||||
static Locale instance_;
|
||||
return instance_;
|
||||
}
|
||||
|
||||
auto Locale::load(const std::string& file_path) -> bool {
|
||||
// Normalitza traient prefix "data/" com fa StageLoader: el pack de
|
||||
// recursos indexa rutes relatives a `data/`.
|
||||
std::string normalized = file_path;
|
||||
if (normalized.starts_with("data/")) {
|
||||
normalized = normalized.substr(5);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> bytes = Resource::Helper::loadFile(normalized);
|
||||
if (bytes.empty()) {
|
||||
std::cerr << "[Locale] no s'ha pogut load " << normalized << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
std::string yaml_content(bytes.begin(), bytes.end());
|
||||
std::stringstream stream(yaml_content);
|
||||
fkyaml::node yaml = fkyaml::node::deserialize(stream);
|
||||
strings_.clear();
|
||||
flatten(yaml, "", strings_);
|
||||
std::cout << "[Locale] " << strings_.size() << " traduccions des de " << normalized << '\n';
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "[Locale] error parsejant " << normalized << ": " << e.what() << '\n';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
auto Locale::switchTo(const std::string& lang) -> bool {
|
||||
return load("locale/" + lang + ".yaml");
|
||||
}
|
||||
|
||||
auto Locale::text(const std::string& key) const -> std::string {
|
||||
auto it = strings_.find(key);
|
||||
if (it != strings_.end()) {
|
||||
return it->second;
|
||||
}
|
||||
std::cerr << "[Locale] clau no trobada: " << key << '\n';
|
||||
return key;
|
||||
}
|
||||
|
||||
auto Locale::count(const std::string& prefix) const -> std::size_t {
|
||||
std::size_t n = 0;
|
||||
while (strings_.contains(prefix + "." + std::to_string(n))) {
|
||||
n++;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
auto localeSubstitute(std::string tpl, std::string_view placeholder, std::string_view value) -> std::string {
|
||||
auto pos = tpl.find(placeholder);
|
||||
if (pos != std::string::npos) {
|
||||
tpl.replace(pos, placeholder.size(), value);
|
||||
}
|
||||
return tpl;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// locale.hpp - Sistema d'internacionalització (i18n) basat en YAML
|
||||
// © 2026 JailDesigner
|
||||
//
|
||||
// Locale amb claus en notació de punts ("notification.fullscreen_on"). El YAML
|
||||
// pot ser jerarquitzat i s'aplana en càrrega, així el consumidor només veu
|
||||
// claus planes. Suporta seqüències de strings (es desen com prefix.0,
|
||||
// prefix.1, ...). No hi ha hot-swap d'idioma: es fixa a l'arrencada des de
|
||||
// `config.yaml` (camp `locale`) i només es recarrega reiniciant el joc.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
|
||||
class Locale {
|
||||
public:
|
||||
static auto get() -> Locale&;
|
||||
|
||||
Locale(const Locale&) = delete;
|
||||
Locale(Locale&&) = delete;
|
||||
auto operator=(const Locale&) -> Locale& = delete;
|
||||
auto operator=(Locale&&) -> Locale& = delete;
|
||||
|
||||
// Llig el fitxer YAML i emplena el mapping intern. Si hi ha un error de
|
||||
// parse o el fitxer no existeix, deixa el mapping com estava i ho
|
||||
// notifica per stderr. Retorna true només si la càrrega ha tingut èxit.
|
||||
auto load(const std::string& file_path) -> bool;
|
||||
|
||||
// Canvi d'idioma en runtime. Recarrega `locale/<lang>.yaml`. Retorna true
|
||||
// si la càrrega ha tingut èxit. Els lookups posteriors (tots els draw*
|
||||
// criden Locale::text() cada frame) ja veuen el nou idioma. Els missatges
|
||||
// ja capturats (toast actiu, banner de stage start ja triat) sobreviuen
|
||||
// fins al seu cicle natural.
|
||||
auto switchTo(const std::string& lang) -> bool;
|
||||
|
||||
// Retorna la traducció; si la clau no existeix, retorna la pròpia clau
|
||||
// com a fallback visible (així una clau mal escrita es detecta sense
|
||||
// trencar el render).
|
||||
[[nodiscard]] auto text(const std::string& key) const -> std::string;
|
||||
|
||||
// Compta quantes claus consecutives existeixen amb el prefix donat
|
||||
// (prefix.0, prefix.1, ...). Útil per pools indexats com stage.start.N.
|
||||
[[nodiscard]] auto count(const std::string& prefix) const -> std::size_t;
|
||||
|
||||
private:
|
||||
Locale() = default;
|
||||
~Locale() = default;
|
||||
|
||||
std::unordered_map<std::string, std::string> strings_;
|
||||
};
|
||||
|
||||
// Substitució simple d'un placeholder dins una plantilla (p.ex. "{n}" → "3").
|
||||
// S'usa per interpolar valors runtime en strings traduïdes.
|
||||
[[nodiscard]] auto localeSubstitute(std::string tpl, std::string_view placeholder, std::string_view value) -> std::string;
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "core/entities/entity.hpp"
|
||||
#include "core/types.hpp"
|
||||
|
||||
@@ -15,7 +17,7 @@ inline auto checkCollision(const Entities::Entity& a, const Entities::Entity& b,
|
||||
return false;
|
||||
}
|
||||
|
||||
// Calcular radi combinat (con amplificador per hitbox generós)
|
||||
// Calcular radius combinat (con amplificador per hitbox generós)
|
||||
float suma_radis = (a.getCollisionRadius() + b.getCollisionRadius()) * amplifier;
|
||||
float suma_radis_sq = suma_radis * suma_radis;
|
||||
|
||||
@@ -29,4 +31,34 @@ inline auto checkCollision(const Entities::Entity& a, const Entities::Entity& b,
|
||||
return dist_sq <= suma_radis_sq;
|
||||
}
|
||||
|
||||
// Swept collision: una entitat mòbil (radius r_a) s'ha desplaçat de p0 a p1 aquest
|
||||
// frame. Comprova si el segment expandit pel radius conjunt (r_a + radius de b, amb
|
||||
// amplificador) toca el cercle de l'entity b. Equival al check discrete quan
|
||||
// p0 == p1 (sense moviment). Evita tunneling a velocitats altes.
|
||||
inline auto checkCollisionSwept(const Vec2& p0, const Vec2& p1, float r_a, const Entities::Entity& b, float amplifier = 1.0F) -> bool {
|
||||
if (!b.isCollidable()) {
|
||||
return false;
|
||||
}
|
||||
const float SUM_R = (r_a + b.getCollisionRadius()) * amplifier;
|
||||
const float SUM_R_SQ = SUM_R * SUM_R;
|
||||
const Vec2& center_b = b.getCenter();
|
||||
const float DX_SEG = p1.x - p0.x;
|
||||
const float DY_SEG = p1.y - p0.y;
|
||||
const float LEN_SQ = (DX_SEG * DX_SEG) + (DY_SEG * DY_SEG);
|
||||
// Degenerat: punt-cercle (frame de spawn, o entitat parada).
|
||||
if (LEN_SQ <= 0.0F) {
|
||||
const float DX = p0.x - center_b.x;
|
||||
const float DY = p0.y - center_b.y;
|
||||
return ((DX * DX) + (DY * DY)) <= SUM_R_SQ;
|
||||
}
|
||||
// Projecció del centre sobre la recta del segment, clamp a [0,1] per acotar al segment.
|
||||
const float T_RAW = (((center_b.x - p0.x) * DX_SEG) + ((center_b.y - p0.y) * DY_SEG)) / LEN_SQ;
|
||||
const float T_CLAMPED = std::clamp(T_RAW, 0.0F, 1.0F);
|
||||
const float CLOSEST_X = p0.x + (DX_SEG * T_CLAMPED);
|
||||
const float CLOSEST_Y = p0.y + (DY_SEG * T_CLAMPED);
|
||||
const float DX = CLOSEST_X - center_b.x;
|
||||
const float DY = CLOSEST_Y - center_b.y;
|
||||
return ((DX * DX) + (DY * DY)) <= SUM_R_SQ;
|
||||
}
|
||||
|
||||
} // namespace Physics
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
#include "core/physics/physics_world.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
#include "core/physics/rigid_body.hpp"
|
||||
@@ -14,10 +13,13 @@ namespace Physics {
|
||||
if (body == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (std::ranges::find(bodies_, body) == bodies_.end()) {
|
||||
bodies_.push_back(body);
|
||||
for (const auto* b : bodies_) {
|
||||
if (b == body) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
bodies_.push_back(body);
|
||||
}
|
||||
|
||||
void PhysicsWorld::removeBody(RigidBody* body) {
|
||||
std::erase(bodies_, body);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user