Compare commits
99 Commits
| 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 |
@@ -9,6 +9,9 @@ Checks:
|
|||||||
- -bugprone-easily-swappable-parameters
|
- -bugprone-easily-swappable-parameters
|
||||||
- -bugprone-narrowing-conversions
|
- -bugprone-narrowing-conversions
|
||||||
- -modernize-avoid-c-arrays
|
- -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)
|
# performance-noexcept-move-constructor crashea clang-tidy (LLVM 19.1)
|
||||||
# con recursión infinita en ExceptionSpecAnalyzer::analyzeRecord cuando
|
# con recursión infinita en ExceptionSpecAnalyzer::analyzeRecord cuando
|
||||||
# analiza ciertas instanciaciones de std::set. No es un falso positivo
|
# analiza ciertas instanciaciones de std::set. No es un falso positivo
|
||||||
|
|||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
cmake_minimum_required(VERSION 3.16)
|
cmake_minimum_required(VERSION 3.16)
|
||||||
project(orni VERSION 0.8.0 LANGUAGES CXX)
|
project(orni VERSION 0.8.1 LANGUAGES CXX)
|
||||||
|
|
||||||
# Info del projecte (font de veritat per a project.h)
|
# Info del projecte (font de veritat per a project.h)
|
||||||
set(PROJECT_LONG_NAME "Orni Attack")
|
set(PROJECT_LONG_NAME "Orni Attack")
|
||||||
@@ -69,7 +69,7 @@ target_link_libraries(${PROJECT_NAME} PRIVATE SDL3::SDL3)
|
|||||||
if(EXTERNAL_SOURCES)
|
if(EXTERNAL_SOURCES)
|
||||||
set_source_files_properties(
|
set_source_files_properties(
|
||||||
${EXTERNAL_SOURCES}
|
${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()
|
endif()
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
endif
|
||||||
|
|
||||||
.PHONY: all debug release _windows-release _macos-release _linux-release \
|
.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
|
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Ó
|
# COMPILACIÓ
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
@@ -98,10 +107,12 @@ endif
|
|||||||
all:
|
all:
|
||||||
@cmake -S . -B $(BUILDDIR) $(CMAKE_GEN) -DCMAKE_BUILD_TYPE=Release $(CMAKE_DEFS)
|
@cmake -S . -B $(BUILDDIR) $(CMAKE_GEN) -DCMAKE_BUILD_TYPE=Release $(CMAKE_DEFS)
|
||||||
@cmake --build $(BUILDDIR) -j$(JOBS)
|
@cmake --build $(BUILDDIR) -j$(JOBS)
|
||||||
|
$(CP_CONTROLLERDB)
|
||||||
|
|
||||||
debug:
|
debug:
|
||||||
@cmake -S . -B $(BUILDDIR) $(CMAKE_GEN) -DCMAKE_BUILD_TYPE=Debug $(CMAKE_DEFS)
|
@cmake -S . -B $(BUILDDIR) $(CMAKE_GEN) -DCMAKE_BUILD_TYPE=Debug $(CMAKE_DEFS)
|
||||||
@cmake --build $(BUILDDIR) -j$(JOBS)
|
@cmake --build $(BUILDDIR) -j$(JOBS)
|
||||||
|
$(CP_CONTROLLERDB)
|
||||||
|
|
||||||
run: all
|
run: all
|
||||||
@./$(BUILDDIR)/$(PROJECT)
|
@./$(BUILDDIR)/$(PROJECT)
|
||||||
@@ -138,6 +149,7 @@ _linux-release:
|
|||||||
|
|
||||||
# Còpia de fitxers
|
# Còpia de fitxers
|
||||||
cp $(BUILDDIR)/resources.pack "$(RELEASE_FOLDER)"
|
cp $(BUILDDIR)/resources.pack "$(RELEASE_FOLDER)"
|
||||||
|
cp gamecontrollerdb.txt "$(RELEASE_FOLDER)"
|
||||||
cp README.md "$(RELEASE_FOLDER)"
|
cp README.md "$(RELEASE_FOLDER)"
|
||||||
@[ -f LICENSE ] && cp LICENSE "$(RELEASE_FOLDER)" || true
|
@[ -f LICENSE ] && cp LICENSE "$(RELEASE_FOLDER)" || true
|
||||||
cp "$(TARGET_FILE)" "$(RELEASE_FILE)"
|
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 "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 -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 "if (Test-Path 'LICENSE') { Copy-Item 'LICENSE' -Destination '$(RELEASE_FOLDER)' }"
|
||||||
@powershell -Command "Copy-Item 'README.md' -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)' }"
|
@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
|
# Compila la versió Apple Silicon
|
||||||
@cmake -S . -B $(BUILDDIR)/arm $(CMAKE_GEN) -DCMAKE_BUILD_TYPE=Release \
|
@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)
|
-DMACOS_BUNDLE=ON $(CMAKE_DEFS)
|
||||||
@cmake --build $(BUILDDIR)/arm -j$(JOBS)
|
@cmake --build $(BUILDDIR)/arm -j$(JOBS)
|
||||||
|
|
||||||
@@ -206,6 +219,7 @@ _macos-release:
|
|||||||
|
|
||||||
# Còpia de recursos i metadades del bundle
|
# Còpia de recursos i metadades del bundle
|
||||||
cp $(BUILDDIR)/arm/resources.pack "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
|
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 -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/icons/icon.icns "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
|
||||||
cp release/macos/Info.plist "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents"
|
cp release/macos/Info.plist "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents"
|
||||||
@@ -274,6 +288,19 @@ pack:
|
|||||||
@cmake --build $(BUILDDIR) --target pack_resources
|
@cmake --build $(BUILDDIR) --target pack_resources
|
||||||
@./$(BUILDDIR)/pack_resources data $(BUILDDIR)/resources.pack
|
@./$(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)
|
# CODE QUALITY (delegats a cmake)
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ name: bullet
|
|||||||
# Shape de la bala. El bounding_radius del .shp dóna el hitbox base (~3 px);
|
# Shape de la bala. El bounding_radius del .shp dóna el hitbox base (~3 px);
|
||||||
# scale el modula visualment i pel hitbox.
|
# scale el modula visualment i pel hitbox.
|
||||||
shape:
|
shape:
|
||||||
path: bullet.shp
|
path: bullet/basic.shp
|
||||||
scale: 1.0
|
scale: 1.0
|
||||||
collision_factor: 1.0
|
collision_factor: 1.0
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -2,7 +2,7 @@ name: pentagon
|
|||||||
ai_type: pentagon # Validat contra el directori; mapeja a EnemyType::PENTAGON.
|
ai_type: pentagon # Validat contra el directori; mapeja a EnemyType::PENTAGON.
|
||||||
|
|
||||||
shape:
|
shape:
|
||||||
path: enemy_pentagon.shp
|
path: enemy/pentagon.shp
|
||||||
scale: 1.0 # multiplicador visual + hitbox sobre la mida nativa del .shp
|
scale: 1.0 # multiplicador visual + hitbox sobre la mida nativa del .shp
|
||||||
collision_factor: 1.0 # ajust opcional del hitbox (default 1.0)
|
collision_factor: 1.0 # ajust opcional del hitbox (default 1.0)
|
||||||
|
|
||||||
@@ -55,10 +55,14 @@ colors:
|
|||||||
score: 100
|
score: 100
|
||||||
|
|
||||||
events:
|
events:
|
||||||
# Comportament clàssic: dos impactes per matar (set_hurt entra wounded;
|
# HP=1 (default): decrement → on_no_health → set_hurt → wounded → mort.
|
||||||
# el segon hit detecta wounded i destrueix automàticament).
|
# 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:
|
on_hit:
|
||||||
|
- action: decrease_health
|
||||||
- action: apply_impulse
|
- action: apply_impulse
|
||||||
|
on_no_health:
|
||||||
- action: set_hurt
|
- action: set_hurt
|
||||||
on_hurt_end:
|
on_hurt_end:
|
||||||
- action: destroy
|
- action: destroy
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ name: pinwheel
|
|||||||
ai_type: pinwheel # Validat contra el directori; mapeja a EnemyType::PINWHEEL.
|
ai_type: pinwheel # Validat contra el directori; mapeja a EnemyType::PINWHEEL.
|
||||||
|
|
||||||
shape:
|
shape:
|
||||||
path: enemy_pinwheel.shp
|
path: enemy/pinwheel.shp
|
||||||
scale: 1.0 # multiplicador visual + hitbox sobre la mida nativa del .shp
|
scale: 1.0 # multiplicador visual + hitbox sobre la mida nativa del .shp
|
||||||
collision_factor: 1.0 # ajust opcional del hitbox (default 1.0)
|
collision_factor: 1.0 # ajust opcional del hitbox (default 1.0)
|
||||||
|
|
||||||
@@ -55,8 +55,11 @@ colors:
|
|||||||
score: 200
|
score: 200
|
||||||
|
|
||||||
events:
|
events:
|
||||||
|
# HP=1 (default): decrement → on_no_health → set_hurt → wounded → mort.
|
||||||
on_hit:
|
on_hit:
|
||||||
|
- action: decrease_health
|
||||||
- action: apply_impulse
|
- action: apply_impulse
|
||||||
|
on_no_health:
|
||||||
- action: set_hurt
|
- action: set_hurt
|
||||||
on_hurt_end:
|
on_hurt_end:
|
||||||
- action: destroy
|
- action: destroy
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
name: player_ship
|
name: player_ship
|
||||||
|
|
||||||
# Shape de la nau. Resolt per ShapeLoader (busca a "shapes/<path>").
|
# Shape de la nau. Resolt per ShapeLoader (busca a "shapes/<path>").
|
||||||
# Nota: el segon jugador rep un override del shape ("ship2.shp") al ctor.
|
# 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
|
# Quan s'introdueixin variants reals de nau, es crearà un YAML separat
|
||||||
# per cada model.
|
# per cada model.
|
||||||
#
|
#
|
||||||
@@ -10,7 +10,7 @@ name: player_ship
|
|||||||
# automàtic de la shape; tocar només si el feel del hitbox
|
# automàtic de la shape; tocar només si el feel del hitbox
|
||||||
# no quadra amb la silueta visual (default 1.0).
|
# no quadra amb la silueta visual (default 1.0).
|
||||||
shape:
|
shape:
|
||||||
path: ship.shp
|
path: ship/arrow.shp
|
||||||
scale: 1.0
|
scale: 1.0
|
||||||
collision_factor: 1.0
|
collision_factor: 1.0
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ visual_thrust:
|
|||||||
|
|
||||||
colors:
|
colors:
|
||||||
normal: [255, 255, 255] # blanc neutre
|
normal: [255, 255, 255] # blanc neutre
|
||||||
hurt: [255, 220, 60] # daurat (estat ferit)
|
hurt: [255, 0, 0] # roig pur (estat ferit)
|
||||||
|
|
||||||
weapon:
|
weapon:
|
||||||
bullet_speed: 700.0 # velocitat escalar de la bullet (px/s)
|
bullet_speed: 700.0 # velocitat escalar de la bullet (px/s)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ name: square
|
|||||||
ai_type: square # Validat contra el directori; mapeja a EnemyType::SQUARE.
|
ai_type: square # Validat contra el directori; mapeja a EnemyType::SQUARE.
|
||||||
|
|
||||||
shape:
|
shape:
|
||||||
path: enemy_square.shp
|
path: enemy/square.shp
|
||||||
scale: 1.0 # multiplicador visual + hitbox sobre la mida nativa del .shp
|
scale: 1.0 # multiplicador visual + hitbox sobre la mida nativa del .shp
|
||||||
collision_factor: 1.0 # ajust opcional del hitbox (default 1.0)
|
collision_factor: 1.0 # ajust opcional del hitbox (default 1.0)
|
||||||
|
|
||||||
@@ -15,10 +15,11 @@ physics:
|
|||||||
linear_damping: 0.0
|
linear_damping: 0.0
|
||||||
angular_damping: 0.0
|
angular_damping: 0.0
|
||||||
|
|
||||||
behavior:
|
ai:
|
||||||
# Square: tracking discret cap a la nau cada N segons.
|
# Square: persecució contínua del ship més proper (steering suau, "tanc lent").
|
||||||
tracking_strength: 0.5 # Interpolació LERP cap a la direcció desitjada (0..1)
|
movement:
|
||||||
tracking_interval: 1.0 # segons entre updates d'angle
|
type: chase
|
||||||
|
chase_strength: 0.5 # Força/segon de la LERP cap a la direcció ideal (1.0 = ~1s per realinear)
|
||||||
|
|
||||||
animation:
|
animation:
|
||||||
pulse:
|
pulse:
|
||||||
@@ -55,8 +56,11 @@ colors:
|
|||||||
score: 150
|
score: 150
|
||||||
|
|
||||||
events:
|
events:
|
||||||
|
# HP=1 (default): decrement → on_no_health → set_hurt → wounded → mort.
|
||||||
on_hit:
|
on_hit:
|
||||||
|
- action: decrease_health
|
||||||
- action: apply_impulse
|
- action: apply_impulse
|
||||||
|
on_no_health:
|
||||||
- action: set_hurt
|
- action: set_hurt
|
||||||
on_hurt_end:
|
on_hurt_end:
|
||||||
- action: destroy
|
- action: destroy
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ name: star
|
|||||||
ai_type: star # Validat contra el directori; mapeja a EnemyType::STAR.
|
ai_type: star # Validat contra el directori; mapeja a EnemyType::STAR.
|
||||||
|
|
||||||
shape:
|
shape:
|
||||||
path: star_5.shp
|
path: enemy/star.shp
|
||||||
scale: 0.7 # Lleugerament més petit que els altres enemics per diferenciar visualment.
|
scale: 0.7 # Lleugerament més petit que els altres enemics per diferenciar visualment.
|
||||||
collision_factor: 1.0
|
collision_factor: 1.0
|
||||||
|
|
||||||
@@ -15,10 +15,20 @@ physics:
|
|||||||
linear_damping: 0.0
|
linear_damping: 0.0
|
||||||
angular_damping: 0.0
|
angular_damping: 0.0
|
||||||
|
|
||||||
behavior:
|
ai:
|
||||||
# Hereta el comportament de Pentagon (zigzag esquivador).
|
# Movement: zigzag esquivador (com Pentagon).
|
||||||
|
movement:
|
||||||
|
type: zigzag
|
||||||
angle_change_max: 1.0
|
angle_change_max: 1.0
|
||||||
zigzag_prob_per_second: 0.8
|
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:
|
animation:
|
||||||
pulse:
|
pulse:
|
||||||
@@ -56,8 +66,10 @@ score: 100
|
|||||||
|
|
||||||
events:
|
events:
|
||||||
# STAR: mor al primer impacte, sense passar per wounded.
|
# STAR: mor al primer impacte, sense passar per wounded.
|
||||||
|
# HP=1 (default): decrement → on_no_health → destroy directe (sense wounded).
|
||||||
on_hit:
|
on_hit:
|
||||||
- action: apply_impulse
|
- action: decrease_health
|
||||||
|
on_no_health:
|
||||||
- action: destroy
|
- action: destroy
|
||||||
on_destroy:
|
on_destroy:
|
||||||
- action: add_score
|
- action: add_score
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ notification:
|
|||||||
antialias_off: "AA INACTIU"
|
antialias_off: "AA INACTIU"
|
||||||
postfx_on: "POSTPROCESSAT ACTIU"
|
postfx_on: "POSTPROCESSAT ACTIU"
|
||||||
postfx_off: "POSTPROCESSAT INACTIU"
|
postfx_off: "POSTPROCESSAT INACTIU"
|
||||||
|
screenshot: "IMATGE {file} GUARDADA A {folder}"
|
||||||
locale_switched: "IDIOMA: {lang}"
|
locale_switched: "IDIOMA: {lang}"
|
||||||
gamepad_connected: "{name} CONNECTAT"
|
gamepad_connected: "{name} CONNECTAT"
|
||||||
gamepad_disconnected: "{name} DESCONNECTAT"
|
gamepad_disconnected: "{name} DESCONNECTAT"
|
||||||
@@ -27,6 +28,9 @@ hud:
|
|||||||
title:
|
title:
|
||||||
press_start: "PREMEU START PER JUGAR"
|
press_start: "PREMEU START PER JUGAR"
|
||||||
|
|
||||||
|
demo:
|
||||||
|
banner: "MODE DEMO - PREMEU START"
|
||||||
|
|
||||||
game_screen:
|
game_screen:
|
||||||
game_over: "FI DEL JOC"
|
game_over: "FI DEL JOC"
|
||||||
continue: "CONTINUAR"
|
continue: "CONTINUAR"
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ notification:
|
|||||||
antialias_off: "AA OFF"
|
antialias_off: "AA OFF"
|
||||||
postfx_on: "POSTPROCESS ON"
|
postfx_on: "POSTPROCESS ON"
|
||||||
postfx_off: "POSTPROCESS OFF"
|
postfx_off: "POSTPROCESS OFF"
|
||||||
|
screenshot: "IMAGE {file} SAVED AT {folder}"
|
||||||
locale_switched: "LANGUAGE: {lang}"
|
locale_switched: "LANGUAGE: {lang}"
|
||||||
gamepad_connected: "{name} CONNECTED"
|
gamepad_connected: "{name} CONNECTED"
|
||||||
gamepad_disconnected: "{name} DISCONNECTED"
|
gamepad_disconnected: "{name} DISCONNECTED"
|
||||||
@@ -26,6 +27,9 @@ hud:
|
|||||||
title:
|
title:
|
||||||
press_start: "PRESS START TO PLAY"
|
press_start: "PRESS START TO PLAY"
|
||||||
|
|
||||||
|
demo:
|
||||||
|
banner: "DEMO MODE - PRESS START"
|
||||||
|
|
||||||
game_screen:
|
game_screen:
|
||||||
game_over: "GAME OVER"
|
game_over: "GAME OVER"
|
||||||
continue: "CONTINUE"
|
continue: "CONTINUE"
|
||||||
|
|||||||
@@ -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
|
scale: 1.0
|
||||||
center: 0, 0
|
center: 0, 0
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# bullet_double.shp - Bala anular (dos cercles concèntrics)
|
# bullet/double.shp - Bala anular (dos cercles concèntrics)
|
||||||
# © 2026 JailDesigner
|
# © 2026 JailDesigner
|
||||||
#
|
#
|
||||||
# Dos octàgons concèntrics al centre (0,0):
|
# Dos octàgons concèntrics al centre (0,0):
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
# - Interior: radi 2 (lleugerament més petit que la bala estàndard)
|
# - Interior: radi 2 (lleugerament més petit que la bala estàndard)
|
||||||
# Aspecte d'anell / aura de plasma. Bounding radius natiu = 4.
|
# Aspecte d'anell / aura de plasma. Bounding radius natiu = 4.
|
||||||
|
|
||||||
name: bullet_double
|
name: double
|
||||||
scale: 1.0
|
scale: 1.0
|
||||||
center: 0, 0
|
center: 0, 0
|
||||||
|
|
||||||
@@ -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,28 +0,0 @@
|
|||||||
# bullet_long.shp - Bala allargada (dos octàgons tangents + tapes superior i inferior)
|
|
||||||
# © 2026 JailDesigner
|
|
||||||
#
|
|
||||||
# Dos cercles (octàgons radi 3) tangents externament al punt (0,0), units
|
|
||||||
# per una línia horitzontal superior i una d'inferior. La silueta resultant
|
|
||||||
# és una càpsula amb la separació visible dels dos cercles al centre.
|
|
||||||
#
|
|
||||||
# Geometria:
|
|
||||||
# Centre octàgon esquerre: (-3, 0)
|
|
||||||
# Centre octàgon dret: ( 3, 0)
|
|
||||||
# Punt de tangència: ( 0, 0)
|
|
||||||
# Bounding radius natiu ≈ 6 (extrem horitzontal a x=±6).
|
|
||||||
|
|
||||||
name: bullet_long
|
|
||||||
scale: 1.0
|
|
||||||
center: 0, 0
|
|
||||||
|
|
||||||
# Octàgon esquerre (centre x=-3, radi 3)
|
|
||||||
polyline: -3,-3 -0.88,-2.12 0,0 -0.88,2.12 -3,3 -5.12,2.12 -6,0 -5.12,-2.12 -3,-3
|
|
||||||
|
|
||||||
# Octàgon dret (centre x=3, radi 3)
|
|
||||||
polyline: 3,-3 5.12,-2.12 6,0 5.12,2.12 3,3 0.88,2.12 0,0 0.88,-2.12 3,-3
|
|
||||||
|
|
||||||
# Tapa superior: uneix el cim de l'octàgon esquerre amb el del dret
|
|
||||||
polyline: -3,-3 3,-3
|
|
||||||
|
|
||||||
# Tapa inferior: uneix la base de l'octàgon esquerre amb la del dret
|
|
||||||
polyline: -3,3 3,3
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# star.shp - Estrella per a starfield
|
# effect/starfield.shp - Estrella per a starfield
|
||||||
# © 2026 JailDesigner
|
# © 2026 JailDesigner
|
||||||
|
|
||||||
name: star
|
name: starfield
|
||||||
scale: 1.0
|
scale: 1.0
|
||||||
center: 0, 0
|
center: 0, 0
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# title_flash.shp - Sparkle 4-puntes amb costats còncaus (Atari-style)
|
# 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
|
# 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.
|
# quadràtica amb control point ±8). 5 punts per arc subdividint la corba.
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# enemy_pentagon.shp - ORNI enemic (pentàgon doble concentric, radi exterior=20)
|
# enemy/pentagon.shp - ORNI enemic (pentàgon doble concentric, radi exterior=20)
|
||||||
|
|
||||||
name: enemy_pentagon
|
name: pentagon
|
||||||
scale: 1.0
|
scale: 1.0
|
||||||
center: 0, 0
|
center: 0, 0
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# enemy_pinwheel.shp - ORNI enemic (molinillo de 4 triangles)
|
# enemy/pinwheel.shp - ORNI enemic (molinillo de 4 triangles)
|
||||||
# © 2026 JailDesigner
|
# © 2026 JailDesigner
|
||||||
|
|
||||||
name: enemy_pinwheel
|
name: pinwheel
|
||||||
scale: 1.0
|
scale: 1.0
|
||||||
center: 0, 0
|
center: 0, 0
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# enemy_square.shp - ORNI enemic (rombe, radi=20) + ull amb pupil·la al centre
|
# enemy/square.shp - ORNI enemic (rombe, radi=20) + ull amb pupil·la al centre
|
||||||
|
|
||||||
name: enemy_square
|
name: square
|
||||||
scale: 1.0
|
scale: 1.0
|
||||||
center: 0, 0
|
center: 0, 0
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# star_5.shp - ORNI enemic (estrella de 5 puntes, només perímetre)
|
# enemy/star.shp - ORNI enemic (estrella de 5 puntes, només perímetre)
|
||||||
# © 2026 JailDesigner
|
# © 2026 JailDesigner
|
||||||
#
|
#
|
||||||
# Pentagrama clàssic: 5 vèrtexs exteriors (radi 20) alternant amb 5 vèrtexs
|
# Pentagrama clàssic: 5 vèrtexs exteriors (radi 20) alternant amb 5 vèrtexs
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
# Sense línies interiors: una única polyline que recorre el perímetre.
|
# Sense línies interiors: una única polyline que recorre el perímetre.
|
||||||
# Bounding radius natiu ≈ 20 (alineat amb pentagon/square/pinwheel).
|
# Bounding radius natiu ≈ 20 (alineat amb pentagon/square/pinwheel).
|
||||||
|
|
||||||
name: star_5
|
name: star
|
||||||
scale: 1.0
|
scale: 1.0
|
||||||
center: 0, 0
|
center: 0, 0
|
||||||
|
|
||||||
@@ -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
|
# © 2026 JailDesigner
|
||||||
|
|
||||||
name: ship2
|
name: interceptor
|
||||||
scale: 1.0
|
scale: 1.0
|
||||||
center: 0, 0
|
center: 0, 0
|
||||||
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
# ship2.shp - Nau del jugador 2
|
# ship/wedge.shp - Nau del jugador 2 (triangle amb cercle central)
|
||||||
# Triangle amb cercle central (distintiu visual)
|
|
||||||
|
|
||||||
name: ship2
|
name: wedge
|
||||||
scale: 1.0
|
scale: 1.0
|
||||||
center: 0, 0
|
center: 0, 0
|
||||||
|
|
||||||
Binary file not shown.
+174
-160
@@ -1,169 +1,183 @@
|
|||||||
# stages.yaml - Configuració de les 10 etapes d'Orni Attack
|
# stages.yaml - Configuració de les fases d'Orni Attack
|
||||||
# © 2026 JailDesigner
|
# © 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:
|
metadata:
|
||||||
version: "1.0"
|
version: "2.0"
|
||||||
total_stages: 10
|
total_stages: 10
|
||||||
description: "Progressive difficulty curve from novice to expert"
|
description: "Wave-based progression"
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
# STAGE 1: Tutorial - Mix de tots 4 tipus al 25% per mostrar-los junts
|
# 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
|
- stage_id: 1
|
||||||
total_enemies: 50
|
multipliers: { velocity: 0.85, rotation: 0.9, tracking: 0.3 }
|
||||||
spawn_config:
|
waves:
|
||||||
mode: "progressive"
|
- spawn: [pentagon, pentagon, orb]
|
||||||
initial_delay: 0.3
|
|
||||||
spawn_interval: 0.4
|
|
||||||
enemy_distribution:
|
|
||||||
pentagon: 25
|
|
||||||
cuadrado: 25
|
|
||||||
molinillo: 25
|
|
||||||
star: 25
|
|
||||||
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
|
|
||||||
spawn_interval: 0.6
|
spawn_interval: 0.6
|
||||||
enemy_distribution:
|
next: all_dead
|
||||||
pentagon: 15
|
- spawn: [pentagon, pentagon, square]
|
||||||
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
|
|
||||||
spawn_interval: 0.5
|
spawn_interval: 0.5
|
||||||
enemy_distribution:
|
next: all_dead
|
||||||
pentagon: 10
|
- spawn: [pentagon, pentagon, square, square]
|
||||||
cuadrado: 20
|
spawn_interval: 0.4
|
||||||
molinillo: 70
|
next: end
|
||||||
difficulty_multipliers:
|
|
||||||
speed_multiplier: 1.8
|
# STAGE 2 — Apareixen molinillos.
|
||||||
rotation_multiplier: 2.0
|
- stage_id: 2
|
||||||
tracking_strength: 1.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>
|
<key>CSResourcesFileMapped</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
<string>10.15</string>
|
<string>13.3</string>
|
||||||
<key>NSHighResolutionCapable</key>
|
<key>NSHighResolutionCapable</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>NSHumanReadableCopyright</key>
|
<key>NSHumanReadableCopyright</key>
|
||||||
|
|||||||
@@ -281,6 +281,17 @@ void Audio::enableMusic(bool value) {
|
|||||||
setMusicVolume(config_.music_volume);
|
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.
|
// Inicialitza SDL Audio y el motor Ja::Engine owned.
|
||||||
void Audio::initSDLAudio() {
|
void Audio::initSDLAudio() {
|
||||||
if (!SDL_Init(SDL_INIT_AUDIO)) {
|
if (!SDL_Init(SDL_INIT_AUDIO)) {
|
||||||
|
|||||||
@@ -128,6 +128,12 @@ class Audio {
|
|||||||
// --- Configuración de sons ---
|
// --- Configuración de sons ---
|
||||||
void enableSound(bool value); // Estableix l'estat dels sons (reaplica volum)
|
void enableSound(bool value); // Estableix l'estat dels sons (reaplica volum)
|
||||||
void toggleSound() { enableSound(!sound_enabled_); } // Alterna 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 ---
|
// --- Configuración de música ---
|
||||||
void enableMusic(bool value); // Estableix l'estat de la música (reaplica volum)
|
void enableMusic(bool value); // Estableix l'estat de la música (reaplica volum)
|
||||||
|
|||||||
@@ -35,10 +35,11 @@ namespace Defaults::Music {
|
|||||||
namespace Defaults::Sound {
|
namespace Defaults::Sound {
|
||||||
|
|
||||||
constexpr const char* CONTINUE = "effects/continue.wav"; // Cuenta atras
|
constexpr const char* CONTINUE = "effects/continue.wav"; // Cuenta atras
|
||||||
constexpr const char* EXPLOSION = "effects/explosion.wav"; // Explosión
|
constexpr const char* ENEMY_EXPLOSION = "effects/enemy_explosion.wav"; // Explosió d'enemic (debris default)
|
||||||
constexpr const char* EXPLOSION2 = "effects/explosion2.wav"; // Explosión alternativa
|
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* 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* 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* INIT_HUD = "effects/init_hud.wav"; // Para la animación del HUD
|
||||||
constexpr const char* LASER = "effects/laser_shoot.wav"; // Disparo
|
constexpr const char* LASER = "effects/laser_shoot.wav"; // Disparo
|
||||||
|
|||||||
@@ -16,3 +16,30 @@ namespace Defaults::Enemies::Spawn {
|
|||||||
constexpr int MAX_SPAWN_ATTEMPTS = 50;
|
constexpr int MAX_SPAWN_ATTEMPTS = 50;
|
||||||
|
|
||||||
} // namespace Defaults::Enemies::Spawn
|
} // namespace Defaults::Enemies::Spawn
|
||||||
|
|
||||||
|
namespace Defaults::Enemies::Visual {
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
} // namespace Defaults::Enemies::Visual
|
||||||
|
|
||||||
|
namespace Defaults::Enemies::Debris {
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
} // namespace Defaults::Enemies::Debris
|
||||||
|
|
||||||
|
namespace Defaults::Enemies::Fireworks {
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
} // namespace Defaults::Enemies::Fireworks
|
||||||
|
|||||||
@@ -3,10 +3,24 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
namespace Defaults::Entities {
|
namespace Defaults::Entities {
|
||||||
|
|
||||||
constexpr int MAX_ORNIS = 15;
|
constexpr int MAX_ORNIS = 15;
|
||||||
constexpr int MAX_BULLETS = 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
|
||||||
|
|
||||||
|
// 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
|
// SHIP_RADIUS / ENEMY_RADIUS / BULLET_RADIUS han migrat: ara cada entitat
|
||||||
// calcula el seu collision_radius com a
|
// calcula el seu collision_radius com a
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ namespace Defaults::Game {
|
|||||||
constexpr int HEIGHT = 720;
|
constexpr int HEIGHT = 720;
|
||||||
|
|
||||||
// Regles de partida
|
// 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 DEATH_DURATION = 3.0F; // Seconds of death animation
|
||||||
constexpr float GAME_OVER_DURATION = 5.0F; // Seconds to display game over
|
constexpr float GAME_OVER_DURATION = 5.0F; // Seconds to display game over
|
||||||
|
|
||||||
@@ -39,6 +40,19 @@ namespace Defaults::Game {
|
|||||||
constexpr float LEVEL_COMPLETED_DURATION = 3.0F; // Duración total
|
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)
|
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)
|
// Transición INIT_HUD (animación inicial del HUD)
|
||||||
constexpr float INIT_HUD_DURATION = 3.0F; // Duración total del estado
|
constexpr float INIT_HUD_DURATION = 3.0F; // Duración total del estado
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
|
|
||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
namespace Defaults::Hud {
|
namespace Defaults::Hud {
|
||||||
|
|
||||||
// Marcador (scoreboard inferior). Usado por GameScene::drawScoreboard()
|
// 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_SCALE = 0.85F;
|
||||||
constexpr float SCOREBOARD_TEXT_SPACING = 0.0F;
|
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).
|
// Animación de entrada del HUD (init_hud_animator).
|
||||||
namespace InitAnim {
|
namespace InitAnim {
|
||||||
// Spawn vertical de la nave: 50 px bajo la PLAYAREA (sale desde fuera).
|
// Spawn vertical de la nave: 50 px bajo la PLAYAREA (sale desde fuera).
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
// Paleta semántica por tipo de entidad. Si una entity declara color, lo
|
// 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
|
// pasa al pipeline con alpha=255 (sentinela "color válido"); si no,
|
||||||
// usa el color global del oscilador (g_current_line_color).
|
// line_renderer::linea() cau a DEFAULT_LINE_COLOR (verd fòsfor fallback).
|
||||||
namespace Defaults::Palette {
|
namespace Defaults::Palette {
|
||||||
|
|
||||||
// Paleta neon: pujada lleugera dels canals secundaris per millorar la
|
// Paleta neon: pujada lleugera dels canals secundaris per millorar la
|
||||||
|
|||||||
@@ -43,6 +43,13 @@ namespace Defaults::Physics::Debris {
|
|||||||
// 1.0 = inèrcia completa; >1.0 amplifica la deriva; <1.0 la atenua.
|
// 1.0 = inèrcia completa; >1.0 amplifica la deriva; <1.0 la atenua.
|
||||||
constexpr float ENEMY_VELOCITY_INHERITANCE = 1.0F;
|
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
|
// Tuneig específic de l'explosió d'enemic (overrides als defaults
|
||||||
// que es passen com a paràmetres opcionals a explode()).
|
// 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
|
constexpr float ENEMY_LIFETIME = 2.5F; // Vida mínima del debris (s) — els que segueixen movent-se viuen més
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <array>
|
#include <array>
|
||||||
|
|
||||||
namespace Defaults::Rendering {
|
namespace Defaults::Rendering {
|
||||||
@@ -35,8 +34,12 @@ namespace Defaults::Rendering {
|
|||||||
constexpr int RENDER_HEIGHT_DEFAULT = 720;
|
constexpr int RENDER_HEIGHT_DEFAULT = 720;
|
||||||
|
|
||||||
constexpr auto isValidRenderResolution(int w, int h) -> bool {
|
constexpr auto isValidRenderResolution(int w, int h) -> bool {
|
||||||
return std::ranges::any_of(RESOLUTION_PRESETS,
|
for (const auto& preset : RESOLUTION_PRESETS) {
|
||||||
[w, h](const ResolutionPreset& preset) { return preset.w == w && preset.h == h; });
|
if (preset.w == w && preset.h == h) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace Defaults::Rendering
|
} // namespace Defaults::Rendering
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ namespace Defaults::Title {
|
|||||||
namespace Layout {
|
namespace Layout {
|
||||||
// Posicions verticals (anclatges des del TOP de pantalla lógica, 0.0-1.0)
|
// Posicions verticals (anclatges des del TOP de pantalla lógica, 0.0-1.0)
|
||||||
constexpr float LOGO_POS = 0.20F; // Logo "ORNI"
|
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
|
constexpr float COPYRIGHT1_POS = 0.90F; // Primera línia copyright
|
||||||
|
|
||||||
// Separacions relatives (proporció respecte Game::HEIGHT = 480px)
|
// Separacions relatives (proporció respecte Game::HEIGHT = 480px)
|
||||||
@@ -116,6 +116,13 @@ namespace Defaults::Title {
|
|||||||
constexpr float LOGO_SCALE = 0.6F; // Escala "ORNI ATTACK!"
|
constexpr float LOGO_SCALE = 0.6F; // Escala "ORNI ATTACK!"
|
||||||
constexpr float PRESS_START_SCALE = 1.0F; // Escala "PRESS START TO PLAY"
|
constexpr float PRESS_START_SCALE = 1.0F; // Escala "PRESS START TO PLAY"
|
||||||
constexpr float COPYRIGHT_SCALE = 0.5F; // Escala copyright
|
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
|
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).
|
// Separación entre el logo JAILGAMES y la línea de copyright (proporción de Game::HEIGHT).
|
||||||
@@ -157,14 +164,18 @@ namespace Defaults::Title {
|
|||||||
// alpha = 255 (sentinela "color vàlid") fa que el pipeline ignori
|
// alpha = 255 (sentinela "color vàlid") fa que el pipeline ignori
|
||||||
// el color global de l'oscil·lador per a aquesta crida.
|
// el color global de l'oscil·lador per a aquesta crida.
|
||||||
namespace Colors {
|
namespace Colors {
|
||||||
constexpr SDL_Color LOGO_MAIN = {.r = 80, .g = 240, .b = 255, .a = 255}; // Cian elèctric
|
// Ambre neon: el mateix to dels missatges d'inici/fi de fase
|
||||||
constexpr SDL_Color LOGO_SHADOW = {.r = 255, .g = 60, .b = 180, .a = 255}; // Magenta neon (offset)
|
// (STAGE_MESSAGE_COLOR a game_scene.cpp) per unificar el feel.
|
||||||
constexpr SDL_Color SHIP_P1 = {.r = 255, .g = 100, .b = 200, .a = 255}; // Rosa hot
|
constexpr SDL_Color AMBER = {.r = 255, .g = 200, .b = 70, .a = 255};
|
||||||
constexpr SDL_Color SHIP_P2 = {.r = 160, .g = 120, .b = 255, .a = 255}; // Violeta elèctric
|
|
||||||
constexpr SDL_Color STARFIELD = {.r = 200, .g = 220, .b = 255, .a = 255}; // Blanc-blau gel
|
constexpr SDL_Color STARFIELD = {.r = 200, .g = 220, .b = 255, .a = 255}; // Blanc-blau gel
|
||||||
constexpr SDL_Color PRESS_START = {.r = 255, .g = 200, .b = 70, .a = 255}; // Ambre neon
|
constexpr SDL_Color LOGO_MAIN = {.r = 0, .g = 255, .b = 255, .a = 255}; // Cian pur
|
||||||
constexpr SDL_Color JAILGAMES_LOGO = {.r = 120, .g = 220, .b = 200, .a = 255}; // Teal suau
|
constexpr SDL_Color LOGO_SHADOW = STARFIELD; // Color de l'starfield (offset)
|
||||||
constexpr SDL_Color COPYRIGHT = {.r = 140, .g = 180, .b = 200, .a = 255}; // Gris-cian apagat
|
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 Colors
|
||||||
|
|
||||||
} // namespace Defaults::Title
|
} // namespace Defaults::Title
|
||||||
|
|||||||
@@ -51,12 +51,12 @@ namespace Graphics {
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
// Lerp de l'oscil·lador (color base actual) cap a un color "flash" en
|
// Lerp del color base verd fòsfor cap a un color "flash" en funció de
|
||||||
// funció de f ∈ [0, 1]. Retorna sempre amb alpha>0 perquè el line_renderer
|
// f ∈ [0, 1]. Retorna sempre amb alpha>0 perquè el line_renderer l'usi
|
||||||
// l'use directament (sense barrejar amb el global).
|
// directament (sense caure al fallback DEFAULT_LINE_COLOR).
|
||||||
auto lerpColor(SDL_Color flash, float f) -> SDL_Color {
|
auto lerpColor(SDL_Color flash, float f) -> SDL_Color {
|
||||||
const float CLAMPED = std::clamp(f, 0.0F, 1.0F);
|
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 auto LERP_U8 = [&](unsigned char a, unsigned char b) {
|
||||||
const float OUT = (static_cast<float>(a) * (1.0F - CLAMPED)) + (static_cast<float>(b) * CLAMPED);
|
const float OUT = (static_cast<float>(a) * (1.0F - CLAMPED)) + (static_cast<float>(b) * CLAMPED);
|
||||||
return static_cast<unsigned char>(OUT);
|
return static_cast<unsigned char>(OUT);
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -62,6 +62,12 @@ namespace Graphics {
|
|||||||
buildLines();
|
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) {
|
void Playfield::update(float delta_time) {
|
||||||
elapsed_s_ += delta_time;
|
elapsed_s_ += delta_time;
|
||||||
for (auto& ripple : ripples_) {
|
for (auto& ripple : ripples_) {
|
||||||
|
|||||||
@@ -30,6 +30,11 @@ namespace Graphics {
|
|||||||
// Avança timers interns (creació + ripples).
|
// Avança timers interns (creació + ripples).
|
||||||
void update(float delta_time);
|
void update(float delta_time);
|
||||||
|
|
||||||
|
// 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,
|
// 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.
|
// i s'aplica deformació radial per cada ripple activa que afecti la línia.
|
||||||
void draw() const;
|
void draw() const;
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ namespace Graphics {
|
|||||||
return it->second; // Cache hit
|
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"
|
// "logo/letra_j.shp" → "shapes/logo/letra_j.shp"
|
||||||
std::string normalized = filename;
|
std::string normalized = filename;
|
||||||
if (!normalized.starts_with("shapes/")) {
|
if (!normalized.starts_with("shapes/")) {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ namespace Graphics {
|
|||||||
|
|
||||||
// Carregar shape desde file (con caché)
|
// Carregar shape desde file (con caché)
|
||||||
// Retorna punter compartit (nullptr si error)
|
// 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>;
|
static auto load(const std::string& filename) -> std::shared_ptr<Shape>;
|
||||||
|
|
||||||
// Netejar caché (útil per debug/recàrrega)
|
// Netejar caché (útil per debug/recàrrega)
|
||||||
|
|||||||
@@ -11,8 +11,9 @@
|
|||||||
namespace Graphics {
|
namespace Graphics {
|
||||||
|
|
||||||
// Constants para mides base dels caràcters
|
// Constants para mides base dels caràcters
|
||||||
constexpr float BASE_CHAR_WIDTH = 20.0F; // Amplada 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
|
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)
|
VectorText::VectorText(Rendering::Renderer* renderer)
|
||||||
: renderer_(renderer) {
|
: renderer_(renderer) {
|
||||||
@@ -47,7 +48,7 @@ namespace Graphics {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cargar símbolos
|
// Cargar símbolos
|
||||||
const std::string SYMBOLS[] = {".", ",", "-", ":", "!", "?", "/", "(", ")"};
|
const std::string SYMBOLS[] = {".", ",", "-", ":", "!", "?", "/", "(", ")", "_"};
|
||||||
for (const auto& sym : SYMBOLS) {
|
for (const auto& sym : SYMBOLS) {
|
||||||
char c = sym[0];
|
char c = sym[0];
|
||||||
std::string filename = getShapeFilename(c);
|
std::string filename = getShapeFilename(c);
|
||||||
@@ -170,6 +171,8 @@ namespace Graphics {
|
|||||||
return "font/char_lparen.shp";
|
return "font/char_lparen.shp";
|
||||||
case ')':
|
case ')':
|
||||||
return "font/char_rparen.shp";
|
return "font/char_rparen.shp";
|
||||||
|
case '_':
|
||||||
|
return "font/char_underscore.shp";
|
||||||
case ' ':
|
case ' ':
|
||||||
return ""; // Espai es maneja sin load shape
|
return ""; // Espai es maneja sin load shape
|
||||||
|
|
||||||
@@ -182,6 +185,10 @@ namespace Graphics {
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto VectorText::isSupported(char c) const -> bool {
|
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);
|
return chars_.contains(c);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,6 +227,14 @@ namespace Graphics {
|
|||||||
continue;
|
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
|
// Verificar si el carácter está soportado
|
||||||
auto it = chars_.find(c);
|
auto it = chars_.find(c);
|
||||||
if (it != chars_.end()) {
|
if (it != chars_.end()) {
|
||||||
@@ -287,4 +302,8 @@ namespace Graphics {
|
|||||||
return BASE_CHAR_HEIGHT * scale;
|
return BASE_CHAR_HEIGHT * scale;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto VectorText::getGlyphHeight(float scale) -> float {
|
||||||
|
return BASE_GLYPH_HEIGHT * scale;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace Graphics
|
} // namespace Graphics
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ namespace Graphics {
|
|||||||
// - scale: factor de scale (1.0 = 20×40 px por carácter)
|
// - scale: factor de scale (1.0 = 20×40 px por carácter)
|
||||||
// - spacing: espacio entre caracteres en píxeles (a scale 1.0)
|
// - 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)
|
// - 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;
|
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
|
// Renderizar string centrado en un punto
|
||||||
@@ -35,7 +35,7 @@ namespace Graphics {
|
|||||||
// - scale: factor de scale (1.0 = 20×40 px por carácter)
|
// - scale: factor de scale (1.0 = 20×40 px por carácter)
|
||||||
// - spacing: espacio entre caracteres en píxeles (a scale 1.0)
|
// - 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)
|
// - 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 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;
|
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).
|
// Calcular ancho total de un string (útil para centrado).
|
||||||
@@ -46,6 +46,10 @@ namespace Graphics {
|
|||||||
// Calcular altura del texto (útil para centrado vertical).
|
// Calcular altura del texto (útil para centrado vertical).
|
||||||
[[nodiscard]] static auto getTextHeight(float scale = 1.0F) -> float;
|
[[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
|
// Verificar si un carácter está soportado
|
||||||
[[nodiscard]] auto isSupported(char c) const -> bool;
|
[[nodiscard]] auto isSupported(char c) const -> bool;
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
// Mesh3D = llista de vèrtexs Vec3 + llista d'arestes (parells d'índexs).
|
// Mesh3D = llista de vèrtexs Vec3 + llista d'arestes (parells d'índexs).
|
||||||
// drawWireframe() aplica una Transform3D al mesh, projecta amb Camera3D i
|
// drawWireframe() aplica una Transform3D al mesh, projecta amb Camera3D i
|
||||||
// emet cada aresta com una línia 2D pel pipeline `Rendering::linea` (mateix
|
// 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
|
// Sense depth buffer: el caller és responsable d'ordenar els meshos per
|
||||||
// profunditat decreixent si vol oclusió coherent (la pipeline és LINE_LIST
|
// profunditat decreixent si vol oclusió coherent (la pipeline és LINE_LIST
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
#include "core/input/define_inputs.hpp"
|
#include "core/input/define_inputs.hpp"
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <format>
|
#include <format>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -154,9 +153,12 @@ namespace System {
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto DefineInputs::isInUse(int code) const -> bool {
|
auto DefineInputs::isInUse(int code) const -> bool {
|
||||||
return std::ranges::any_of(sequence_, [code](const Step& s) {
|
for (const auto& s : sequence_) {
|
||||||
return s.captured == code;
|
if (s.captured == code) {
|
||||||
});
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void DefineInputs::captureAndAdvance(int code) {
|
void DefineInputs::captureAndAdvance(int code) {
|
||||||
|
|||||||
+19
-11
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
#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 <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 <iostream> // Para basic_ostream, operator<<, cout, cerr
|
||||||
#include <memory> // Para shared_ptr, __shared_ptr_access, allocator, operator==, make_shared
|
#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 <unordered_map> // Para unordered_map, _Node_iterator, operator==, _Node_iterator_base, _Node_const_iterator
|
||||||
@@ -166,9 +165,12 @@ auto Input::checkAnyButton(bool repeat) -> bool {
|
|||||||
|
|
||||||
// Comprueba si algún player (P1 o P2) presionó alguna acción de una lista
|
// 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 {
|
auto Input::checkAnyPlayerAction(const std::span<const InputAction>& actions, bool repeat) -> bool {
|
||||||
return std::ranges::any_of(actions, [this, repeat](const InputAction& action) {
|
for (const auto& action : actions) {
|
||||||
return checkActionPlayer1(action, repeat) || checkActionPlayer2(action, repeat);
|
if (checkActionPlayer1(action, repeat) || checkActionPlayer2(action, repeat)) {
|
||||||
});
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Comprueba si hay algun mando conectado
|
// Comprueba si hay algun mando conectado
|
||||||
@@ -307,8 +309,11 @@ auto Input::checkTriggerInput(Action action, const std::shared_ptr<Gamepad>& gam
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Input::addGamepadMappingsFromFile() {
|
void Input::addGamepadMappingsFromFile() {
|
||||||
if (SDL_AddGamepadMappingsFromFile(gamepad_mappings_file_.c_str()) < 0) {
|
const int COUNT = SDL_AddGamepadMappingsFromFile(gamepad_mappings_file_.c_str());
|
||||||
std::cout << "Error, could not load " << gamepad_mappings_file_.c_str() << " file: " << SDL_GetError() << '\n';
|
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";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,8 +331,7 @@ void Input::initSDLGamePad() {
|
|||||||
} else {
|
} else {
|
||||||
addGamepadMappingsFromFile();
|
addGamepadMappingsFromFile();
|
||||||
discoverGamepads();
|
discoverGamepads();
|
||||||
std::cout << "\n** INPUT SYSTEM **\n";
|
std::cout << "[Input] inicialitzat\n";
|
||||||
std::cout << "Input System initialized successfully\n";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -441,9 +445,13 @@ auto Input::addGamepad(int device_index) -> std::string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto Input::removeGamepad(SDL_JoystickID id) -> std::string {
|
auto Input::removeGamepad(SDL_JoystickID id) -> std::string {
|
||||||
auto it = std::ranges::find_if(gamepads_, [id](const std::shared_ptr<Gamepad>& gamepad) {
|
auto it = gamepads_.end();
|
||||||
return gamepad->instance_id == id;
|
for (auto i = gamepads_.begin(); i != gamepads_.end(); ++i) {
|
||||||
});
|
if ((*i)->instance_id == id) {
|
||||||
|
it = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (it != gamepads_.end()) {
|
if (it != gamepads_.end()) {
|
||||||
std::string name = (*it)->name;
|
std::string name = (*it)->name;
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
#include "core/physics/physics_world.hpp"
|
#include "core/physics/physics_world.hpp"
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
|
||||||
#include "core/physics/rigid_body.hpp"
|
#include "core/physics/rigid_body.hpp"
|
||||||
@@ -14,10 +13,13 @@ namespace Physics {
|
|||||||
if (body == nullptr) {
|
if (body == nullptr) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (std::ranges::find(bodies_, body) == bodies_.end()) {
|
for (const auto* b : bodies_) {
|
||||||
bodies_.push_back(body);
|
if (b == body) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
bodies_.push_back(body);
|
||||||
|
}
|
||||||
|
|
||||||
void PhysicsWorld::removeBody(RigidBody* body) {
|
void PhysicsWorld::removeBody(RigidBody* body) {
|
||||||
std::erase(bodies_, body);
|
std::erase(bodies_, body);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
#include <cstddef>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
@@ -132,6 +133,7 @@ namespace Rendering::GPU {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void GpuFrameRenderer::destroy() {
|
void GpuFrameRenderer::destroy() {
|
||||||
|
destroyCaptureResources();
|
||||||
destroyOffscreen();
|
destroyOffscreen();
|
||||||
postfx_pipeline_.destroy();
|
postfx_pipeline_.destroy();
|
||||||
bloom_pipeline_.destroy();
|
bloom_pipeline_.destroy();
|
||||||
@@ -172,7 +174,7 @@ namespace Rendering::GPU {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!SDL_WaitAndAcquireGPUSwapchainTexture(cmd_buffer_, device_.window(), &swapchain_texture_, nullptr, nullptr)) {
|
if (!SDL_WaitAndAcquireGPUSwapchainTexture(cmd_buffer_, device_.window(), &swapchain_texture_, &swapchain_w_, &swapchain_h_)) {
|
||||||
std::cerr << "[GpuFrameRenderer] WaitAndAcquire: " << SDL_GetError() << '\n';
|
std::cerr << "[GpuFrameRenderer] WaitAndAcquire: " << SDL_GetError() << '\n';
|
||||||
SDL_SubmitGPUCommandBuffer(cmd_buffer_);
|
SDL_SubmitGPUCommandBuffer(cmd_buffer_);
|
||||||
cmd_buffer_ = nullptr;
|
cmd_buffer_ = nullptr;
|
||||||
@@ -540,11 +542,20 @@ namespace Rendering::GPU {
|
|||||||
SDL_EndGPURenderPass(render_pass_);
|
SDL_EndGPURenderPass(render_pass_);
|
||||||
render_pass_ = nullptr;
|
render_pass_ = nullptr;
|
||||||
}
|
}
|
||||||
|
compositeTo(swapchain_texture_);
|
||||||
|
}
|
||||||
|
|
||||||
// Pase final: render pass sobre SWAPCHAIN con clear a negro (cubre el
|
void GpuFrameRenderer::compositeTo(SDL_GPUTexture* target_tex) {
|
||||||
|
// Cos comú del pase de postpro: sample offscreen + bloom → target_tex
|
||||||
|
// (swapchain en el camí normal, o la textura de captura per al
|
||||||
|
// screenshot). El caller s'encarrega de tancar qualsevol render pass
|
||||||
|
// previ. Com que la textura de captura té la mateixa mida que la
|
||||||
|
// swapchain, el mateix viewport/letterbox produeix píxels idèntics.
|
||||||
|
|
||||||
|
// Pase final: render pass sobre el target con clear a negro (cubre el
|
||||||
// letterbox del viewport físico).
|
// letterbox del viewport físico).
|
||||||
SDL_GPUColorTargetInfo target{};
|
SDL_GPUColorTargetInfo target{};
|
||||||
target.texture = swapchain_texture_;
|
target.texture = target_tex;
|
||||||
target.clear_color = SDL_FColor{.r = 0.0F, .g = 0.0F, .b = 0.0F, .a = 1.0F};
|
target.clear_color = SDL_FColor{.r = 0.0F, .g = 0.0F, .b = 0.0F, .a = 1.0F};
|
||||||
target.load_op = SDL_GPU_LOADOP_CLEAR;
|
target.load_op = SDL_GPU_LOADOP_CLEAR;
|
||||||
target.store_op = SDL_GPU_STOREOP_STORE;
|
target.store_op = SDL_GPU_STOREOP_STORE;
|
||||||
@@ -664,14 +675,150 @@ namespace Rendering::GPU {
|
|||||||
}
|
}
|
||||||
flushBatch();
|
flushBatch();
|
||||||
bloomPass();
|
bloomPass();
|
||||||
compositePass();
|
compositePass(); // → swapchain (camí de presentació normal)
|
||||||
if (render_pass_ != nullptr) {
|
if (render_pass_ != nullptr) {
|
||||||
SDL_EndGPURenderPass(render_pass_);
|
SDL_EndGPURenderPass(render_pass_);
|
||||||
render_pass_ = nullptr;
|
render_pass_ = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Captura (screenshot): segon composite sobre la textura pròpia +
|
||||||
|
// readback a CPU. Només quan hi ha petició pendent i els recursos es
|
||||||
|
// poden crear; si no, submit normal i prou. La petició es consumeix
|
||||||
|
// sempre (s'haja pogut capturar o no) per no quedar enganxada.
|
||||||
|
if (capture_requested_ && ensureCaptureResources()) {
|
||||||
|
compositeTo(capture_texture_);
|
||||||
|
if (render_pass_ != nullptr) {
|
||||||
|
SDL_EndGPURenderPass(render_pass_);
|
||||||
|
render_pass_ = nullptr;
|
||||||
|
}
|
||||||
|
downloadCapture(); // fa submit + fence + map; deixa cmd_buffer_ a nullptr
|
||||||
|
} else {
|
||||||
SDL_SubmitGPUCommandBuffer(cmd_buffer_);
|
SDL_SubmitGPUCommandBuffer(cmd_buffer_);
|
||||||
cmd_buffer_ = nullptr;
|
cmd_buffer_ = nullptr;
|
||||||
|
}
|
||||||
|
capture_requested_ = false;
|
||||||
swapchain_texture_ = nullptr;
|
swapchain_texture_ = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto GpuFrameRenderer::ensureCaptureResources() -> bool {
|
||||||
|
SDL_GPUDevice* dev = device_.get();
|
||||||
|
if (dev == nullptr || swapchain_w_ == 0 || swapchain_h_ == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const int W = static_cast<int>(swapchain_w_);
|
||||||
|
const int H = static_cast<int>(swapchain_h_);
|
||||||
|
if (capture_texture_ != nullptr && capture_tex_w_ == W && capture_tex_h_ == H) {
|
||||||
|
return true; // ja són de la mida correcta
|
||||||
|
}
|
||||||
|
destroyCaptureResources();
|
||||||
|
|
||||||
|
// Mateix format que la swapchain: és el target del postfx_pipeline_.
|
||||||
|
SDL_GPUTextureCreateInfo tex_info{};
|
||||||
|
tex_info.type = SDL_GPU_TEXTURETYPE_2D;
|
||||||
|
tex_info.format = device_.swapchainFormat();
|
||||||
|
tex_info.usage = SDL_GPU_TEXTUREUSAGE_COLOR_TARGET;
|
||||||
|
tex_info.width = swapchain_w_;
|
||||||
|
tex_info.height = swapchain_h_;
|
||||||
|
tex_info.layer_count_or_depth = 1;
|
||||||
|
tex_info.num_levels = 1;
|
||||||
|
tex_info.sample_count = SDL_GPU_SAMPLECOUNT_1;
|
||||||
|
capture_texture_ = SDL_CreateGPUTexture(dev, &tex_info);
|
||||||
|
if (capture_texture_ == nullptr) {
|
||||||
|
std::cerr << "[GpuFrameRenderer] CreateGPUTexture (captura): " << SDL_GetError() << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_GPUTransferBufferCreateInfo tbo_info{};
|
||||||
|
tbo_info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_DOWNLOAD;
|
||||||
|
tbo_info.size = static_cast<uint32_t>(W * H * 4);
|
||||||
|
capture_transfer_ = SDL_CreateGPUTransferBuffer(dev, &tbo_info);
|
||||||
|
if (capture_transfer_ == nullptr) {
|
||||||
|
std::cerr << "[GpuFrameRenderer] CreateGPUTransferBuffer (captura): " << SDL_GetError() << '\n';
|
||||||
|
SDL_ReleaseGPUTexture(dev, capture_texture_);
|
||||||
|
capture_texture_ = nullptr;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
capture_tex_w_ = W;
|
||||||
|
capture_tex_h_ = H;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GpuFrameRenderer::downloadCapture() {
|
||||||
|
SDL_GPUDevice* dev = device_.get();
|
||||||
|
|
||||||
|
// Copy pass: descarrega la textura de captura al transfer buffer.
|
||||||
|
SDL_GPUCopyPass* copy_pass = SDL_BeginGPUCopyPass(cmd_buffer_);
|
||||||
|
SDL_GPUTextureRegion src{};
|
||||||
|
src.texture = capture_texture_;
|
||||||
|
src.w = swapchain_w_;
|
||||||
|
src.h = swapchain_h_;
|
||||||
|
src.d = 1;
|
||||||
|
SDL_GPUTextureTransferInfo dst{};
|
||||||
|
dst.transfer_buffer = capture_transfer_;
|
||||||
|
dst.offset = 0;
|
||||||
|
dst.pixels_per_row = swapchain_w_;
|
||||||
|
dst.rows_per_layer = swapchain_h_;
|
||||||
|
SDL_DownloadFromGPUTexture(copy_pass, &src, &dst);
|
||||||
|
SDL_EndGPUCopyPass(copy_pass);
|
||||||
|
|
||||||
|
// Submit amb fence i esperar: el readback ha d'estar complet abans de mapar.
|
||||||
|
SDL_GPUFence* fence = SDL_SubmitGPUCommandBufferAndAcquireFence(cmd_buffer_);
|
||||||
|
cmd_buffer_ = nullptr;
|
||||||
|
if (fence == nullptr) {
|
||||||
|
std::cerr << "[GpuFrameRenderer] SubmitAndAcquireFence (captura): " << SDL_GetError() << '\n';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SDL_WaitForGPUFences(dev, true, &fence, 1);
|
||||||
|
SDL_ReleaseGPUFence(dev, fence);
|
||||||
|
|
||||||
|
const auto* mapped = static_cast<const uint8_t*>(SDL_MapGPUTransferBuffer(dev, capture_transfer_, false));
|
||||||
|
if (mapped == nullptr) {
|
||||||
|
std::cerr << "[GpuFrameRenderer] MapGPUTransferBuffer (captura): " << SDL_GetError() << '\n';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conversió a ARGB8888 (0xAARRGGBB), que és el que espera Screenshot::save.
|
||||||
|
// El swapchain a Linux/Vulkan sol ser B8G8R8A8_UNORM (bytes B,G,R,A);
|
||||||
|
// altrament tractem com R8G8B8A8_UNORM (bytes R,G,B,A). Alpha forçat a 255
|
||||||
|
// perquè el composite ja escriu opac.
|
||||||
|
const int W = static_cast<int>(swapchain_w_);
|
||||||
|
const int H = static_cast<int>(swapchain_h_);
|
||||||
|
const auto COUNT = static_cast<std::size_t>(W) * static_cast<std::size_t>(H);
|
||||||
|
capture_pixels_.resize(COUNT);
|
||||||
|
const bool BGRA = device_.swapchainFormat() == SDL_GPU_TEXTUREFORMAT_B8G8R8A8_UNORM;
|
||||||
|
for (std::size_t i = 0; i < COUNT; ++i) {
|
||||||
|
const uint8_t* px = mapped + (i * 4);
|
||||||
|
const std::uint32_t R = BGRA ? px[2] : px[0];
|
||||||
|
const std::uint32_t G = px[1];
|
||||||
|
const std::uint32_t B = BGRA ? px[0] : px[2];
|
||||||
|
capture_pixels_[i] = (0xFFU << 24) | (R << 16) | (G << 8) | B;
|
||||||
|
}
|
||||||
|
SDL_UnmapGPUTransferBuffer(dev, capture_transfer_);
|
||||||
|
|
||||||
|
capture_w_ = W;
|
||||||
|
capture_h_ = H;
|
||||||
|
capture_ready_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GpuFrameRenderer::destroyCaptureResources() {
|
||||||
|
SDL_GPUDevice* dev = device_.get();
|
||||||
|
if (dev == nullptr) {
|
||||||
|
capture_texture_ = nullptr;
|
||||||
|
capture_transfer_ = nullptr;
|
||||||
|
capture_tex_w_ = 0;
|
||||||
|
capture_tex_h_ = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (capture_texture_ != nullptr) {
|
||||||
|
SDL_ReleaseGPUTexture(dev, capture_texture_);
|
||||||
|
capture_texture_ = nullptr;
|
||||||
|
}
|
||||||
|
if (capture_transfer_ != nullptr) {
|
||||||
|
SDL_ReleaseGPUTransferBuffer(dev, capture_transfer_);
|
||||||
|
capture_transfer_ = nullptr;
|
||||||
|
}
|
||||||
|
capture_tex_w_ = 0;
|
||||||
|
capture_tex_h_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace Rendering::GPU
|
} // namespace Rendering::GPU
|
||||||
|
|||||||
@@ -133,6 +133,19 @@ namespace Rendering::GPU {
|
|||||||
void setPostFxEnabled(bool enabled) { postfx_enabled_ = enabled; }
|
void setPostFxEnabled(bool enabled) { postfx_enabled_ = enabled; }
|
||||||
[[nodiscard]] auto isPostFxEnabled() const -> bool { return postfx_enabled_; }
|
[[nodiscard]] auto isPostFxEnabled() const -> bool { return postfx_enabled_; }
|
||||||
|
|
||||||
|
// === Captura de pantalla (screenshot) ===
|
||||||
|
// Demana una captura del frame actual: el pròxim endFrame fa un segon
|
||||||
|
// composite sobre una textura pròpia (mida swapchain) i el descarrega a
|
||||||
|
// CPU en ARGB8888. Cost zero quan no hi ha cap petició pendent. Després
|
||||||
|
// del present, el caller comprova hasCapture(), llig captureData() i
|
||||||
|
// crida clearCapture().
|
||||||
|
void requestCapture() { capture_requested_ = true; }
|
||||||
|
[[nodiscard]] auto hasCapture() const -> bool { return capture_ready_; }
|
||||||
|
[[nodiscard]] auto captureData() const -> const std::uint32_t* { return capture_pixels_.data(); }
|
||||||
|
[[nodiscard]] auto captureWidth() const -> int { return capture_w_; }
|
||||||
|
[[nodiscard]] auto captureHeight() const -> int { return capture_h_; }
|
||||||
|
void clearCapture() { capture_ready_ = false; }
|
||||||
|
|
||||||
// Acceso a internals.
|
// Acceso a internals.
|
||||||
[[nodiscard]] auto device() -> GpuDevice& { return device_; }
|
[[nodiscard]] auto device() -> GpuDevice& { return device_; }
|
||||||
[[nodiscard]] auto isInsideFrame() const -> bool { return cmd_buffer_ != nullptr; }
|
[[nodiscard]] auto isInsideFrame() const -> bool { return cmd_buffer_ != nullptr; }
|
||||||
@@ -173,6 +186,26 @@ namespace Rendering::GPU {
|
|||||||
SDL_GPUTexture* bloom_texture_a_{nullptr};
|
SDL_GPUTexture* bloom_texture_a_{nullptr};
|
||||||
SDL_GPUTexture* bloom_texture_b_{nullptr};
|
SDL_GPUTexture* bloom_texture_b_{nullptr};
|
||||||
|
|
||||||
|
// === Captura de pantalla ===
|
||||||
|
// Dimensions reals de la swapchain (capturades a beginFrame). El target
|
||||||
|
// de captura es crea a aquesta mida perquè el PNG surti exactament com
|
||||||
|
// es veu en pantalla (amb letterbox), no a la resolució del offscreen.
|
||||||
|
uint32_t swapchain_w_{0};
|
||||||
|
uint32_t swapchain_h_{0};
|
||||||
|
// Textura on es fa el segon composite (mateix format que la swapchain) i
|
||||||
|
// transfer buffer per a descarregar-la a CPU. Es recreen si canvia la
|
||||||
|
// mida de la finestra (capture_tex_w_/h_ = mida amb què es van crear).
|
||||||
|
SDL_GPUTexture* capture_texture_{nullptr};
|
||||||
|
SDL_GPUTransferBuffer* capture_transfer_{nullptr};
|
||||||
|
int capture_tex_w_{0};
|
||||||
|
int capture_tex_h_{0};
|
||||||
|
// Píxels descarregats (ARGB8888, 0xAARRGGBB) i estat de la petició.
|
||||||
|
std::vector<std::uint32_t> capture_pixels_;
|
||||||
|
int capture_w_{0};
|
||||||
|
int capture_h_{0};
|
||||||
|
bool capture_requested_{false};
|
||||||
|
bool capture_ready_{false};
|
||||||
|
|
||||||
// Batch del frame en curso.
|
// Batch del frame en curso.
|
||||||
std::vector<LineVertex> vertices_;
|
std::vector<LineVertex> vertices_;
|
||||||
std::vector<uint16_t> indices_;
|
std::vector<uint16_t> indices_;
|
||||||
@@ -202,8 +235,14 @@ namespace Rendering::GPU {
|
|||||||
void flushBatch();
|
void flushBatch();
|
||||||
void bloomPass(); // pre-composite: H + V passes sobre les bloom textures
|
void bloomPass(); // pre-composite: H + V passes sobre les bloom textures
|
||||||
void compositePass();
|
void compositePass();
|
||||||
|
void compositeTo(SDL_GPUTexture* target_tex); // cos comú del pase de postpro
|
||||||
void applyFinalViewport();
|
void applyFinalViewport();
|
||||||
void applyCurrentScissor(); // re-aplica el top de clip_stack_ al render_pass_
|
void applyCurrentScissor(); // re-aplica el top de clip_stack_ al render_pass_
|
||||||
|
|
||||||
|
// Captura: (re)crea recursos a mida swapchain, descarrega a CPU i allibera.
|
||||||
|
[[nodiscard]] auto ensureCaptureResources() -> bool;
|
||||||
|
void downloadCapture(); // copy pass + fence + map → capture_pixels_
|
||||||
|
void destroyCaptureResources();
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace Rendering::GPU
|
} // namespace Rendering::GPU
|
||||||
|
|||||||
@@ -8,11 +8,6 @@
|
|||||||
|
|
||||||
namespace Rendering {
|
namespace Rendering {
|
||||||
|
|
||||||
// Color global compartido para líneas sin paleta propia (HUD, debug, texto
|
|
||||||
// genérico). Equivale al "color máximo" de la antigua oscilación CPU: verde
|
|
||||||
// fósforo CRT. El pulso de brillo lo aplica ahora el shader de postpro.
|
|
||||||
SDL_Color g_current_line_color = {100, 255, 100, 255};
|
|
||||||
|
|
||||||
// Grosor global por defecto. Configurable via setLineThickness.
|
// Grosor global por defecto. Configurable via setLineThickness.
|
||||||
float g_current_line_thickness = Defaults::Rendering::LINE_THICKNESS_DEFAULT;
|
float g_current_line_thickness = Defaults::Rendering::LINE_THICKNESS_DEFAULT;
|
||||||
|
|
||||||
@@ -36,8 +31,8 @@ namespace Rendering {
|
|||||||
const auto FX2 = static_cast<float>(x2);
|
const auto FX2 = static_cast<float>(x2);
|
||||||
const auto FY2 = static_cast<float>(y2);
|
const auto FY2 = static_cast<float>(y2);
|
||||||
|
|
||||||
// color.alpha==0 → usar color global (verde fósforo). alpha>0 → color directo.
|
// color.alpha==0 → fallback a DEFAULT_LINE_COLOR (verd fòsfor). alpha>0 → color directo.
|
||||||
const SDL_Color SOURCE = (color.a > 0) ? color : g_current_line_color;
|
const SDL_Color SOURCE = (color.a > 0) ? color : DEFAULT_LINE_COLOR;
|
||||||
const float R = (static_cast<float>(SOURCE.r) * brightness) / 255.0F;
|
const float R = (static_cast<float>(SOURCE.r) * brightness) / 255.0F;
|
||||||
const float G = (static_cast<float>(SOURCE.g) * brightness) / 255.0F;
|
const float G = (static_cast<float>(SOURCE.g) * brightness) / 255.0F;
|
||||||
const float B = (static_cast<float>(SOURCE.b) * brightness) / 255.0F;
|
const float B = (static_cast<float>(SOURCE.b) * brightness) / 255.0F;
|
||||||
@@ -68,10 +63,6 @@ namespace Rendering {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void setLineColor(SDL_Color color) { g_current_line_color = color; }
|
|
||||||
|
|
||||||
auto getLineColor() -> SDL_Color { return g_current_line_color; }
|
|
||||||
|
|
||||||
void setLineThickness(float thickness) {
|
void setLineThickness(float thickness) {
|
||||||
if (thickness > 0.0F) {
|
if (thickness > 0.0F) {
|
||||||
g_current_line_thickness = thickness;
|
g_current_line_thickness = thickness;
|
||||||
|
|||||||
@@ -3,9 +3,10 @@
|
|||||||
//
|
//
|
||||||
// El dibujo de líneas pasa por el pipeline GPU. Las coordenadas (x1,y1,x2,y2)
|
// El dibujo de líneas pasa por el pipeline GPU. Las coordenadas (x1,y1,x2,y2)
|
||||||
// son lógicas (1280×720); el shader las mapea a NDC y el viewport del SDLManager
|
// son lógicas (1280×720); el shader las mapea a NDC y el viewport del SDLManager
|
||||||
// hace el letterbox a píxeles físicos. El brillo modula el color global de
|
// hace el letterbox a píxeles físicos. El pulse de brillo lo aplica el shader
|
||||||
// línea (lo gestiona ColorOscillator). El grosor es configurable por línea
|
// de postpro (ya no hi ha un ColorOscillator a CPU). El grosor es configurable
|
||||||
// (parámetro thickness>0) o global (g_current_line_thickness vía setLineThickness).
|
// per línia (parámetro thickness>0) o global (g_current_line_thickness vía
|
||||||
|
// setLineThickness).
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
@@ -15,12 +16,17 @@
|
|||||||
|
|
||||||
namespace Rendering {
|
namespace Rendering {
|
||||||
|
|
||||||
|
// Color verd fòsfor CRT per defecte: s'usa quan el caller passa color amb
|
||||||
|
// alpha==0 (sentinella "sense color propi"). Constant immutable: la
|
||||||
|
// semàntica de "color global" ja no existeix (era de l'antic ColorOscillator).
|
||||||
|
constexpr SDL_Color DEFAULT_LINE_COLOR = {.r = 100, .g = 255, .b = 100, .a = 255};
|
||||||
|
|
||||||
// Dibuja una línea entre dos puntos en coordenadas lógicas (1280×720).
|
// Dibuja una línea entre dos puntos en coordenadas lógicas (1280×720).
|
||||||
// brightness: factor de brillo (0.0..1.0, default 1.0 = brillo máximo).
|
// brightness: factor de brillo (0.0..1.0, default 1.0 = brillo máximo).
|
||||||
// Pre-multiplica el RGB del color (color dim sobre fons negre).
|
// Pre-multiplica el RGB del color (color dim sobre fons negre).
|
||||||
// thickness: grosor en píxeles lógicos. Si <= 0 usa g_current_line_thickness.
|
// thickness: grosor en píxeles lógicos. Si <= 0 usa g_current_line_thickness.
|
||||||
// color: si alpha==0, se usa el color global del oscilador; si alpha>0 se
|
// color: si alpha==0, se usa DEFAULT_LINE_COLOR (verd fòsfor fallback);
|
||||||
// usa este color directo (paleta semántica por entidad).
|
// si alpha>0 se usa este color directo (paleta semántica por entidad).
|
||||||
// alpha: alpha que arriba al GPU (default 1.0 = opac, behavior original).
|
// alpha: alpha que arriba al GPU (default 1.0 = opac, behavior original).
|
||||||
// Valors <1.0 fan que la línia es barregi de veritat sobre el dest
|
// Valors <1.0 fan que la línia es barregi de veritat sobre el dest
|
||||||
// en comptes de sobrepintar-lo (útil per halos translúcids).
|
// en comptes de sobrepintar-lo (útil per halos translúcids).
|
||||||
@@ -49,10 +55,6 @@ namespace Rendering {
|
|||||||
SDL_Color color = {0, 0, 0, 0},
|
SDL_Color color = {0, 0, 0, 0},
|
||||||
SDL_Color glow_color = {0, 0, 0, 0});
|
SDL_Color glow_color = {0, 0, 0, 0});
|
||||||
|
|
||||||
// Color global de las líneas (lo actualiza ColorOscillator vía SDLManager).
|
|
||||||
void setLineColor(SDL_Color color);
|
|
||||||
[[nodiscard]] auto getLineColor() -> SDL_Color;
|
|
||||||
|
|
||||||
// Grosor global por defecto (en píxeles lógicos). Default: 1.5.
|
// Grosor global por defecto (en píxeles lógicos). Default: 1.5.
|
||||||
void setLineThickness(float thickness);
|
void setLineThickness(float thickness);
|
||||||
[[nodiscard]] auto getLineThickness() -> float;
|
[[nodiscard]] auto getLineThickness() -> float;
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
#include "core/rendering/screenshot.hpp"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <ctime>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "external/stb_image_write.h"
|
||||||
|
|
||||||
|
namespace Screenshot {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Estat de mòdul: ruta base on s'escriu la subcarpeta
|
||||||
|
// `screenshots/`. Buida per defecte ⇒ relativa al CWD (fallback
|
||||||
|
// si el Director no ha pogut establir la ruta per plataforma).
|
||||||
|
std::string g_base_dir; // NOLINT(*-avoid-non-const-global-variables) -- estat de mòdul (singleton-like): s'estableix una vegada al startup via setBaseDir() i es llegeix N vegades; encapsulat dins del namespace anònim, no és accessible des de fora.
|
||||||
|
|
||||||
|
// Construeix la ruta del directori on van les captures:
|
||||||
|
// `<base>/screenshots/`. Si la base és buida, retorna
|
||||||
|
// "screenshots/" relativa al CWD.
|
||||||
|
auto screenshotsDir() -> std::string {
|
||||||
|
if (g_base_dir.empty()) { return "screenshots/"; }
|
||||||
|
const bool ENDS_WITH_SEP = (g_base_dir.back() == '/' || g_base_dir.back() == '\\');
|
||||||
|
return g_base_dir + (ENDS_WITH_SEP ? "" : "/") + "screenshots/";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Converteix ARGB8888 → RGBA8888 in-place i escriu el PNG. stb_image_write
|
||||||
|
// espera RGBA en ordre byte (little-endian: R al byte baix, A al byte alt).
|
||||||
|
auto writePng(std::uint32_t* buffer, int width, int height, const std::string& filepath) -> bool {
|
||||||
|
const int TOTAL = width * height;
|
||||||
|
for (int i = 0; i < TOTAL; ++i) {
|
||||||
|
const std::uint32_t P = buffer[i];
|
||||||
|
const std::uint32_t A = (P >> 24) & 0xFF;
|
||||||
|
const std::uint32_t R = (P >> 16) & 0xFF;
|
||||||
|
const std::uint32_t G = (P >> 8) & 0xFF;
|
||||||
|
const std::uint32_t B = P & 0xFF;
|
||||||
|
buffer[i] = R | (G << 8) | (B << 16) | (A << 24);
|
||||||
|
}
|
||||||
|
return stbi_write_png(filepath.c_str(), width, height, 4, buffer, width * 4) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void setBaseDir(std::string dir) {
|
||||||
|
g_base_dir = std::move(dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto save(const std::uint32_t* buffer, int width, int height) -> Result {
|
||||||
|
const std::string FOLDER = screenshotsDir();
|
||||||
|
std::error_code ec;
|
||||||
|
std::filesystem::create_directories(FOLDER, ec);
|
||||||
|
|
||||||
|
const auto NOW = std::chrono::system_clock::now();
|
||||||
|
const std::time_t TIME = std::chrono::system_clock::to_time_t(NOW);
|
||||||
|
const std::tm* tm = std::localtime(&TIME);
|
||||||
|
char timestamp[20];
|
||||||
|
std::strftime(timestamp, sizeof(timestamp), "%Y%m%d_%H%M%S", tm);
|
||||||
|
|
||||||
|
const std::string FILENAME = std::string("scr_") + timestamp + ".png";
|
||||||
|
const std::string FILEPATH = FOLDER + FILENAME;
|
||||||
|
|
||||||
|
// Còpia local per no tocar el canvas original durant la conversió.
|
||||||
|
std::vector<std::uint32_t> copy(buffer, buffer + (static_cast<size_t>(width) * height));
|
||||||
|
if (!writePng(copy.data(), width, height, FILEPATH)) { return {}; }
|
||||||
|
return {.filename = FILENAME, .folder = FOLDER};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Screenshot
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
// Volca un buffer ARGB8888 a un fitxer PNG dins de la subcarpeta
|
||||||
|
// `screenshots/` del directori configurat amb `setBaseDir`. El nom es
|
||||||
|
// genera amb un timestamp i la funció retorna el basename i la ruta
|
||||||
|
// del directori on s'ha escrit (o un Result amb `filename` buit si
|
||||||
|
// l'escriptura ha fallat).
|
||||||
|
namespace Screenshot {
|
||||||
|
|
||||||
|
// Configura el directori base on s'escriuran les captures (s'hi
|
||||||
|
// crea una subcarpeta `screenshots/`). El Director el crida una
|
||||||
|
// vegada al boot amb `system_folder_` perquè les captures vagin
|
||||||
|
// al mateix lloc per plataforma que la resta de fitxers generats
|
||||||
|
// (config.yaml, gamepad_configs.yaml, shaders/, etc.). Si no es
|
||||||
|
// crida, el fallback és el CWD — comportament antic.
|
||||||
|
void setBaseDir(std::string dir);
|
||||||
|
|
||||||
|
// Resultat d'una captura: el `filename` és el basename del PNG
|
||||||
|
// (per a notificacions/títol) i el `folder` és la ruta absoluta
|
||||||
|
// on s'ha guardat (per a poder dir "guardada a {folder}"). Si
|
||||||
|
// l'escriptura falla, ambdós camps tornen buits.
|
||||||
|
struct Result {
|
||||||
|
std::string filename;
|
||||||
|
std::string folder;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto save(const std::uint32_t* buffer, int width, int height) -> Result;
|
||||||
|
|
||||||
|
} // namespace Screenshot
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
#include "core/input/mouse.hpp"
|
#include "core/input/mouse.hpp"
|
||||||
#include "core/locale/locale.hpp"
|
#include "core/locale/locale.hpp"
|
||||||
#include "core/rendering/coordinate_transform.hpp"
|
#include "core/rendering/coordinate_transform.hpp"
|
||||||
|
#include "core/rendering/screenshot.hpp"
|
||||||
#include "core/system/notifier.hpp"
|
#include "core/system/notifier.hpp"
|
||||||
#include "project.h"
|
#include "project.h"
|
||||||
|
|
||||||
@@ -360,8 +361,36 @@ auto SDLManager::clear(uint8_t r, uint8_t g, uint8_t b) -> bool {
|
|||||||
return gpu_renderer_.beginFrame(0.0F, 0.0F, 0.0F);
|
return gpu_renderer_.beginFrame(0.0F, 0.0F, 0.0F);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SDLManager::requestScreenshot() {
|
||||||
|
// La captura es fa dins del pròxim endFrame (segon composite + readback);
|
||||||
|
// el resultat es recull ací mateix, a present(), un cop el frame ja s'ha
|
||||||
|
// compost. Així el PNG mostra exactament el que es veu en pantalla.
|
||||||
|
gpu_renderer_.requestCapture();
|
||||||
|
}
|
||||||
|
|
||||||
void SDLManager::present() {
|
void SDLManager::present() {
|
||||||
gpu_renderer_.endFrame();
|
gpu_renderer_.endFrame();
|
||||||
|
|
||||||
|
// Si el frame que s'acaba de presentar portava una captura demanada,
|
||||||
|
// l'escrivim a PNG i notifiquem la ruta. La notificació arriba DESPRÉS de
|
||||||
|
// la captura, així que el toast "guardada" no apareix dins de la imatge.
|
||||||
|
if (gpu_renderer_.hasCapture()) {
|
||||||
|
const auto RESULT = Screenshot::save(
|
||||||
|
gpu_renderer_.captureData(),
|
||||||
|
gpu_renderer_.captureWidth(),
|
||||||
|
gpu_renderer_.captureHeight());
|
||||||
|
gpu_renderer_.clearCapture();
|
||||||
|
if (!RESULT.filename.empty()) {
|
||||||
|
if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
|
||||||
|
std::string msg = localeSubstitute(
|
||||||
|
Locale::get().text("notification.screenshot"),
|
||||||
|
"{file}",
|
||||||
|
RESULT.filename);
|
||||||
|
msg = localeSubstitute(msg, "{folder}", RESULT.folder);
|
||||||
|
notifier->notifyInfo(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void SDLManager::toggleVSync() {
|
void SDLManager::toggleVSync() {
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ class SDLManager {
|
|||||||
void toggleVSync(); // F4
|
void toggleVSync(); // F4
|
||||||
void toggleAntialias(); // F5
|
void toggleAntialias(); // F5
|
||||||
void togglePostFx(); // F6
|
void togglePostFx(); // F6
|
||||||
|
void requestScreenshot(); // F9: demana una captura del pròxim frame
|
||||||
// Canvia la resolució del render target offscreen (recrea la textura).
|
// Canvia la resolució del render target offscreen (recrea la textura).
|
||||||
// Cal cridar-lo fora d'un frame (event phase, no draw phase). Si el
|
// Cal cridar-lo fora d'un frame (event phase, no draw phase). Si el
|
||||||
// valor no es un preset valid o ja es l'actual, es no-op.
|
// valor no es un preset valid o ja es l'actual, es no-op.
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
#include "core/input/input.hpp"
|
#include "core/input/input.hpp"
|
||||||
#include "core/input/mouse.hpp"
|
#include "core/input/mouse.hpp"
|
||||||
#include "core/locale/locale.hpp"
|
#include "core/locale/locale.hpp"
|
||||||
|
#include "core/rendering/screenshot.hpp"
|
||||||
#include "core/rendering/sdl_manager.hpp"
|
#include "core/rendering/sdl_manager.hpp"
|
||||||
#include "core/resources/resource_helper.hpp"
|
#include "core/resources/resource_helper.hpp"
|
||||||
#include "core/resources/resource_loader.hpp"
|
#include "core/resources/resource_loader.hpp"
|
||||||
@@ -99,6 +100,10 @@ Director::Director(int argc, char* argv[])
|
|||||||
// Establir ruta del file de configuración
|
// Establir ruta del file de configuración
|
||||||
ConfigYaml::setConfigFile(system_folder_ + "/config.yaml");
|
ConfigYaml::setConfigFile(system_folder_ + "/config.yaml");
|
||||||
|
|
||||||
|
// Les captures de pantalla van sota la mateixa carpeta per plataforma
|
||||||
|
// (subcarpeta screenshots/). Sense açò, Screenshot::save cauria al CWD.
|
||||||
|
Screenshot::setBaseDir(system_folder_);
|
||||||
|
|
||||||
// Carregar o crear configuración
|
// Carregar o crear configuración
|
||||||
ConfigYaml::loadFromFile();
|
ConfigYaml::loadFromFile();
|
||||||
|
|
||||||
@@ -106,8 +111,11 @@ Director::Director(int argc, char* argv[])
|
|||||||
// falla, Locale::text() retorna la clau crua i el joc segueix funcionant.
|
// falla, Locale::text() retorna la clau crua i el joc segueix funcionant.
|
||||||
Locale::get().load(std::string("locale/") + cfg_->locale + ".yaml");
|
Locale::get().load(std::string("locale/") + cfg_->locale + ".yaml");
|
||||||
|
|
||||||
// Inicialitzar sistema de input
|
// Inicialitzar sistema de input. El gamecontrollerdb.txt viu al costat del
|
||||||
Input::init("data/gamecontrollerdb.txt");
|
// binari (no dins de resources.pack, perquè SDL_AddGamepadMappingsFromFile
|
||||||
|
// necessita una ruta real de filesystem). resource_base ja apunta al directori
|
||||||
|
// de l'executable (o a Contents/Resources en bundles de macOS).
|
||||||
|
Input::init(resource_base + "/gamecontrollerdb.txt");
|
||||||
|
|
||||||
// Autoassignacio de primer arranque: si cap dels dos jugadors te mando
|
// Autoassignacio de primer arranque: si cap dels dos jugadors te mando
|
||||||
// assignat al config, repartim els que hi haja detectats (P1 = pad 0,
|
// assignat al config, repartim els que hi haja detectats (P1 = pad 0,
|
||||||
|
|||||||
@@ -160,6 +160,11 @@ namespace GlobalEvents {
|
|||||||
sdl.togglePostFx();
|
sdl.togglePostFx();
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
case SDL_SCANCODE_F9:
|
||||||
|
// Captura de pantalla (PNG) amb shaders, a mida de finestra.
|
||||||
|
sdl.requestScreenshot();
|
||||||
|
return true;
|
||||||
|
|
||||||
case SDL_SCANCODE_F7: {
|
case SDL_SCANCODE_F7: {
|
||||||
// Toggle d'idioma en runtime entre català i anglès. Els
|
// Toggle d'idioma en runtime entre català i anglès. Els
|
||||||
// strings ja capturats (toast actiu, banner stage start)
|
// strings ja capturats (toast actiu, banner stage start)
|
||||||
|
|||||||
@@ -70,10 +70,24 @@ namespace SceneManager {
|
|||||||
return match_config_;
|
return match_config_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Nombre d'escenaris demo curats (cicle del attract mode).
|
||||||
|
static constexpr std::uint8_t DEMO_SCENARIO_COUNT = 4;
|
||||||
|
|
||||||
|
// Índex de l'escenari demo actual. Persisteix entre transicions (el
|
||||||
|
// SceneContext el posseeix el Director), així cada entrada al mode demo
|
||||||
|
// mostra el següent escenari de la llista curada.
|
||||||
|
[[nodiscard]] auto demoScenarioIndex() const -> std::uint8_t {
|
||||||
|
return demo_scenario_index_;
|
||||||
|
}
|
||||||
|
void advanceDemoScenario() {
|
||||||
|
demo_scenario_index_ = (demo_scenario_index_ + 1) % DEMO_SCENARIO_COUNT;
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
SceneType next_scene_{SceneType::LOGO}; // SceneType a la qual transicionar
|
SceneType next_scene_{SceneType::LOGO}; // SceneType a la qual transicionar
|
||||||
Option option_{Option::NONE}; // Opción específica per l'escena
|
Option option_{Option::NONE}; // Opción específica per l'escena
|
||||||
GameConfig::MatchConfig match_config_; // Configuración de match (jugadors active, mode)
|
GameConfig::MatchConfig match_config_; // Configuración de match (jugadors active, mode)
|
||||||
|
std::uint8_t demo_scenario_index_{0}; // Índex de l'escenari demo (attract mode)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Variable global inline per gestionar l'escena actual (backward compatibility)
|
// Variable global inline per gestionar l'escena actual (backward compatibility)
|
||||||
|
|||||||
Vendored
+1724
File diff suppressed because it is too large
Load Diff
+13
@@ -0,0 +1,13 @@
|
|||||||
|
// Unitat de compilació aïllada per a la implementació de stb_image_write.
|
||||||
|
// Viu dins de source/external/ perquè el `.clang-tidy` d'aquesta carpeta
|
||||||
|
// desactiva tots els checks (com fa per stb_vorbis.c) i el pre-commit hook
|
||||||
|
// ja filtra aquesta ruta de clang-format / clang-tidy. Així els fals
|
||||||
|
// positius de clang-analyzer-* dins de codi de tercers no afecten el
|
||||||
|
// nostre codi, que continua tenint tots els checks actius.
|
||||||
|
//
|
||||||
|
// La resta del codi inclou només el header (sense la macro d'implementació),
|
||||||
|
// que queda només amb declaracions — clang-analyzer no pot trobar cap bug
|
||||||
|
// dins d'una declaració, així que l'inclusió és innòcua.
|
||||||
|
|
||||||
|
#define STB_IMAGE_WRITE_IMPLEMENTATION
|
||||||
|
#include "external/stb_image_write.h"
|
||||||
@@ -57,13 +57,18 @@ namespace Effects {
|
|||||||
SDL_Color color,
|
SDL_Color color,
|
||||||
float lifetime,
|
float lifetime,
|
||||||
float friction,
|
float friction,
|
||||||
int segment_multiplier) {
|
int segment_multiplier,
|
||||||
|
const Vec2& bullet_impulse_velocity,
|
||||||
|
float piece_scale) {
|
||||||
if (!shape || !shape->isValid()) {
|
if (!shape || !shape->isValid()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reproducir sonido de explosión
|
// Reproducir sonido de explosión. Cadena buida = explosió silenciosa
|
||||||
|
// (p. ex. el logo dins el cicle d'atracció).
|
||||||
|
if (!sound.empty()) {
|
||||||
Audio::get()->playSound(sound, Audio::Group::GAME);
|
Audio::get()->playSound(sound, Audio::Group::GAME);
|
||||||
|
}
|
||||||
|
|
||||||
// Notificar als subscriptors (border, playfield, etc.).
|
// Notificar als subscriptors (border, playfield, etc.).
|
||||||
if (explosion_callback_) {
|
if (explosion_callback_) {
|
||||||
@@ -84,7 +89,7 @@ namespace Effects {
|
|||||||
Vec2 world_p2 = transformPoint(local_p2, shape_centre, centro, angle, scale);
|
Vec2 world_p2 = transformPoint(local_p2, shape_centre, centro, angle, scale);
|
||||||
|
|
||||||
// Si el pool es ple, no té sentit continuar amb la resta de segments
|
// Si el pool es ple, no té sentit continuar amb la resta de segments
|
||||||
if (!spawnDebris(world_p1, world_p2, centro, velocitat_base, brightness, velocitat_objecte, velocitat_angular, factor_herencia_visual, color, lifetime, friction)) {
|
if (!spawnDebris(world_p1, world_p2, centro, velocitat_base, brightness, velocitat_objecte, velocitat_angular, factor_herencia_visual, color, lifetime, friction, bullet_impulse_velocity, piece_scale)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -110,34 +115,47 @@ namespace Effects {
|
|||||||
return segments;
|
return segments;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto DebrisManager::spawnDebris(const Vec2& world_p1, const Vec2& world_p2, const Vec2& centro, float velocitat_base, float brightness, const Vec2& velocitat_objecte, float velocitat_angular, float factor_herencia_visual, SDL_Color color, float lifetime, float friction) -> bool {
|
auto DebrisManager::spawnDebris(const Vec2& world_p1_in, const Vec2& world_p2_in, const Vec2& centro, float velocitat_base, float brightness, const Vec2& velocitat_objecte, float velocitat_angular, float factor_herencia_visual, SDL_Color color, float lifetime, float friction, const Vec2& bullet_impulse_velocity, float piece_scale) -> bool {
|
||||||
Debris* debris = findFreeSlot();
|
Debris* debris = findFreeSlot();
|
||||||
if (debris == nullptr) {
|
if (debris == nullptr) {
|
||||||
std::cerr << "[DebrisManager] Warning: no debris slots disponibles\n";
|
std::cerr << "[DebrisManager] Warning: no debris slots disponibles\n";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Escala el segment al voltant del seu punt mitjà segons piece_scale
|
||||||
|
// (1.0 = original; 0.3 = "esquerda petita"). La resta del càlcul (angle,
|
||||||
|
// half_length, p1/p2) en deriva naturalment.
|
||||||
|
const Vec2 MID = {.x = (world_p1_in.x + world_p2_in.x) / 2.0F,
|
||||||
|
.y = (world_p1_in.y + world_p2_in.y) / 2.0F};
|
||||||
|
const Vec2 WORLD_P1 = {.x = MID.x + ((world_p1_in.x - MID.x) * piece_scale),
|
||||||
|
.y = MID.y + ((world_p1_in.y - MID.y) * piece_scale)};
|
||||||
|
const Vec2 WORLD_P2 = {.x = MID.x + ((world_p2_in.x - MID.x) * piece_scale),
|
||||||
|
.y = MID.y + ((world_p2_in.y - MID.y) * piece_scale)};
|
||||||
|
|
||||||
// Geometria autoritaritzada: centro + original_angle + original_half_length.
|
// Geometria autoritaritzada: centro + original_angle + original_half_length.
|
||||||
// p1/p2 es reconstrueixen cada frame en update() des d'aquestes dades.
|
// p1/p2 es reconstrueixen cada frame en update() des d'aquestes dades.
|
||||||
const float DX = world_p2.x - world_p1.x;
|
const float DX = WORLD_P2.x - WORLD_P1.x;
|
||||||
const float DY = world_p2.y - world_p1.y;
|
const float DY = WORLD_P2.y - WORLD_P1.y;
|
||||||
debris->centro = {.x = (world_p1.x + world_p2.x) / 2.0F,
|
debris->centro = {.x = (WORLD_P1.x + WORLD_P2.x) / 2.0F,
|
||||||
.y = (world_p1.y + world_p2.y) / 2.0F};
|
.y = (WORLD_P1.y + WORLD_P2.y) / 2.0F};
|
||||||
debris->original_angle = std::atan2(DY, DX);
|
debris->original_angle = std::atan2(DY, DX);
|
||||||
debris->original_half_length = std::sqrt((DX * DX) + (DY * DY)) / 2.0F;
|
debris->original_half_length = std::sqrt((DX * DX) + (DY * DY)) / 2.0F;
|
||||||
debris->p1 = world_p1;
|
debris->p1 = WORLD_P1;
|
||||||
debris->p2 = world_p2;
|
debris->p2 = WORLD_P2;
|
||||||
|
|
||||||
// Direcció radial (desde el centro hacia el segment)
|
// Direcció radial (desde el centro hacia el segment)
|
||||||
Vec2 direccio = computeExplosionDirection(world_p1, world_p2, centro);
|
Vec2 direccio = computeExplosionDirection(WORLD_P1, WORLD_P2, centro);
|
||||||
|
|
||||||
// Velocidad inicial (base ± variació aleatòria + velocity heretada de l'objecte)
|
// Velocidad inicial (base ± variació aleatòria + velocity heretada de l'objecte +
|
||||||
|
// velocitat de la bala escalada per BULLET_IMPULSE_FACTOR).
|
||||||
float speed =
|
float speed =
|
||||||
velocitat_base +
|
velocitat_base +
|
||||||
(((std::rand() / static_cast<float>(RAND_MAX)) * 2.0F - 1.0F) *
|
(((std::rand() / static_cast<float>(RAND_MAX)) * 2.0F - 1.0F) *
|
||||||
Defaults::Physics::Debris::VARIACIO_SPEED);
|
Defaults::Physics::Debris::VARIACIO_SPEED);
|
||||||
debris->velocity.x = (direccio.x * speed) + velocitat_objecte.x;
|
debris->velocity.x = (direccio.x * speed) + velocitat_objecte.x +
|
||||||
debris->velocity.y = (direccio.y * speed) + velocitat_objecte.y;
|
(bullet_impulse_velocity.x * Defaults::Physics::Debris::BULLET_IMPULSE_FACTOR);
|
||||||
|
debris->velocity.y = (direccio.y * speed) + velocitat_objecte.y +
|
||||||
|
(bullet_impulse_velocity.y * Defaults::Physics::Debris::BULLET_IMPULSE_FACTOR);
|
||||||
debris->acceleration = friction;
|
debris->acceleration = friction;
|
||||||
|
|
||||||
// Rotación de trayectoria (con conversió a tangencial si excedeix cap)
|
// Rotación de trayectoria (con conversió a tangencial si excedeix cap)
|
||||||
|
|||||||
@@ -47,6 +47,14 @@ namespace Effects {
|
|||||||
// - lifetime: temps de vida del debris (s, per defecte TEMPS_VIDA = 2s)
|
// - lifetime: temps de vida del debris (s, per defecte TEMPS_VIDA = 2s)
|
||||||
// - friction: desacceleració del debris (px/s², per defecte ACCELERACIO = -60)
|
// - friction: desacceleració del debris (px/s², per defecte ACCELERACIO = -60)
|
||||||
// - segment_multiplier: nombre de còpies per segment (per defecte 1 = sense duplicar)
|
// - segment_multiplier: nombre de còpies per segment (per defecte 1 = sense duplicar)
|
||||||
|
// - bullet_impulse_velocity: velocitat de la bala que ha causat l'impacte (px/s,
|
||||||
|
// per defecte 0). S'aplica a cada fragment escalada per
|
||||||
|
// Defaults::Physics::Debris::BULLET_IMPULSE_FACTOR, independent de
|
||||||
|
// velocitat_objecte. Permet que els trossos "salten amb la força de la bala"
|
||||||
|
// encara que el cos sigui pesat i amb prou feines es mogui.
|
||||||
|
// - piece_scale: multiplicador de la longitud de cada fragment al spawn
|
||||||
|
// (per defecte 1.0). Útil per a debris "parcial" d'impactes no letals
|
||||||
|
// en enemics HP>1 (trossos petits, com d'esquerda).
|
||||||
void explode(const std::shared_ptr<Graphics::Shape>& shape,
|
void explode(const std::shared_ptr<Graphics::Shape>& shape,
|
||||||
const Vec2& centro,
|
const Vec2& centro,
|
||||||
float angle,
|
float angle,
|
||||||
@@ -56,11 +64,14 @@ namespace Effects {
|
|||||||
const Vec2& velocitat_objecte = {.x = 0.0F, .y = 0.0F},
|
const Vec2& velocitat_objecte = {.x = 0.0F, .y = 0.0F},
|
||||||
float velocitat_angular = 0.0F,
|
float velocitat_angular = 0.0F,
|
||||||
float factor_herencia_visual = 0.0F,
|
float factor_herencia_visual = 0.0F,
|
||||||
const std::string& sound = Defaults::Sound::EXPLOSION,
|
// sound: nom del so d'explosió. Cadena buida ("") = explosió silenciosa.
|
||||||
|
const std::string& sound = Defaults::Sound::ENEMY_EXPLOSION,
|
||||||
SDL_Color color = {0, 0, 0, 0}, // alpha==0 → fragmentos usan oscilador global
|
SDL_Color color = {0, 0, 0, 0}, // alpha==0 → fragmentos usan oscilador global
|
||||||
float lifetime = Defaults::Physics::Debris::TEMPS_VIDA,
|
float lifetime = Defaults::Physics::Debris::TEMPS_VIDA,
|
||||||
float friction = Defaults::Physics::Debris::ACCELERACIO,
|
float friction = Defaults::Physics::Debris::ACCELERACIO,
|
||||||
int segment_multiplier = 1);
|
int segment_multiplier = 1,
|
||||||
|
const Vec2& bullet_impulse_velocity = {.x = 0.0F, .y = 0.0F},
|
||||||
|
float piece_scale = 1.0F);
|
||||||
|
|
||||||
// Actualitzar todos los fragments active
|
// Actualitzar todos los fragments active
|
||||||
void update(float delta_time);
|
void update(float delta_time);
|
||||||
@@ -97,7 +108,7 @@ namespace Effects {
|
|||||||
-> std::vector<std::pair<Vec2, Vec2>>;
|
-> std::vector<std::pair<Vec2, Vec2>>;
|
||||||
// Inicialitza un debris en un slot lliure i el deixa actiu. Retorna
|
// Inicialitza un debris en un slot lliure i el deixa actiu. Retorna
|
||||||
// false si el pool está ple (la cridadora ha d'aturar el bucle).
|
// false si el pool está ple (la cridadora ha d'aturar el bucle).
|
||||||
auto spawnDebris(const Vec2& world_p1, const Vec2& world_p2, const Vec2& centro, float velocitat_base, float brightness, const Vec2& velocitat_objecte, float velocitat_angular, float factor_herencia_visual, SDL_Color color, float lifetime, float friction) -> bool;
|
auto spawnDebris(const Vec2& world_p1, const Vec2& world_p2, const Vec2& centro, float velocitat_base, float brightness, const Vec2& velocitat_objecte, float velocitat_angular, float factor_herencia_visual, SDL_Color color, float lifetime, float friction, const Vec2& bullet_impulse_velocity, float piece_scale) -> bool;
|
||||||
static void applyAngularVelocity(Debris& debris, const Vec2& direccio, float velocitat_angular);
|
static void applyAngularVelocity(Debris& debris, const Vec2& direccio, float velocitat_angular);
|
||||||
static void applyVisualRotation(Debris& debris, float velocitat_angular, float factor_herencia_visual);
|
static void applyVisualRotation(Debris& debris, float velocitat_angular, float factor_herencia_visual);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -33,9 +33,9 @@ namespace Effects {
|
|||||||
|
|
||||||
TrailManager::TrailManager(Rendering::Renderer* renderer)
|
TrailManager::TrailManager(Rendering::Renderer* renderer)
|
||||||
: renderer_(renderer),
|
: renderer_(renderer),
|
||||||
star_shape_(Graphics::ShapeLoader::load("star.shp")) {
|
star_shape_(Graphics::ShapeLoader::load("effect/starfield.shp")) {
|
||||||
if (!star_shape_ || !star_shape_->isValid()) {
|
if (!star_shape_ || !star_shape_->isValid()) {
|
||||||
std::cerr << "[TrailManager] Warning: no s'ha pogut load star.shp\n";
|
std::cerr << "[TrailManager] Warning: no s'ha pogut load effect/starfield.shp\n";
|
||||||
}
|
}
|
||||||
for (auto& particle : pool_) {
|
for (auto& particle : pool_) {
|
||||||
particle.active = false;
|
particle.active = false;
|
||||||
|
|||||||
@@ -54,10 +54,29 @@ void Bullet::init() {
|
|||||||
body_.clearAccumulators();
|
body_.clearAccumulators();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Bullet::fire(const Vec2& position, float angle, uint8_t owner_id, float bullet_speed) {
|
void Bullet::fire(const Vec2& position, float angle, uint8_t owner_id, float bullet_speed, const BulletConfig* cfg) {
|
||||||
is_active_ = true;
|
is_active_ = true;
|
||||||
owner_id_ = owner_id;
|
owner_id_ = owner_id;
|
||||||
|
|
||||||
|
// Si no es passa cfg, restaurem al config per defecte (BulletRegistry::get):
|
||||||
|
// els slots són reutilitzables i una bala que abans ha estat disparada amb
|
||||||
|
// una variant (p.ex. bullet_long d'enemic) ha de tornar al bullet.shp del
|
||||||
|
// player quan aquest la reutilitza.
|
||||||
|
const BulletConfig* effective = (cfg != nullptr) ? cfg : &BulletRegistry::get();
|
||||||
|
if (effective != config_) {
|
||||||
|
config_ = effective;
|
||||||
|
shape_ = Graphics::ShapeLoader::load(config_->shape.path);
|
||||||
|
if (!shape_ || !shape_->isValid()) {
|
||||||
|
std::cerr << "[Bullet] Error: no s'ha pogut carregar " << config_->shape.path << '\n';
|
||||||
|
}
|
||||||
|
const float BOUNDING = (shape_ != nullptr) ? shape_->getBoundingRadius() : 0.0F;
|
||||||
|
collision_radius_ = BOUNDING * config_->shape.scale * config_->shape.collision_factor;
|
||||||
|
body_.setMass(config_->physics.mass);
|
||||||
|
body_.restitution = config_->physics.restitution;
|
||||||
|
body_.linear_damping = config_->physics.linear_damping;
|
||||||
|
body_.angular_damping = config_->physics.angular_damping;
|
||||||
|
}
|
||||||
|
|
||||||
center_ = position;
|
center_ = position;
|
||||||
prev_position_ = position; // spawn: swept degenera a punt-cercle
|
prev_position_ = position; // spawn: swept degenera a punt-cercle
|
||||||
angle_ = angle;
|
angle_ = angle;
|
||||||
@@ -95,6 +114,6 @@ void Bullet::desactivar() {
|
|||||||
|
|
||||||
void Bullet::draw() const {
|
void Bullet::draw() const {
|
||||||
if (is_active_ && shape_) {
|
if (is_active_ && shape_) {
|
||||||
Rendering::renderShape(renderer_, shape_, center_, angle_, 1.0F, 1.0F, brightness_, config_->colors.normal);
|
Rendering::renderShape(renderer_, shape_, center_, angle_, config_->shape.scale, 1.0F, brightness_, config_->colors.normal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,11 @@ class Bullet : public Entities::Entity {
|
|||||||
explicit Bullet(Rendering::Renderer* renderer);
|
explicit Bullet(Rendering::Renderer* renderer);
|
||||||
|
|
||||||
void init() override;
|
void init() override;
|
||||||
void fire(const Vec2& position, float angle, uint8_t owner_id, float bullet_speed);
|
// cfg = nullptr → manté el config actual (per defecte: BulletRegistry::get()).
|
||||||
|
// cfg != nullptr → substitueix el config per a aquesta bala (recarrega
|
||||||
|
// shape, recalcula collision_radius_, mass, etc.). Útil per a bales
|
||||||
|
// d'enemic amb shape pròpia.
|
||||||
|
void fire(const Vec2& position, float angle, uint8_t owner_id, float bullet_speed, const BulletConfig* cfg = nullptr);
|
||||||
void update(float delta_time) override;
|
void update(float delta_time) override;
|
||||||
void postUpdate(float delta_time) override;
|
void postUpdate(float delta_time) override;
|
||||||
void draw() const override;
|
void draw() const override;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// bullet_registry.cpp - Implementació del registre de bala
|
// bullet_registry.cpp - Implementació del registre de bales
|
||||||
// © 2026 JailDesigner
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
#include "game/entities/bullet_registry.hpp"
|
#include "game/entities/bullet_registry.hpp"
|
||||||
@@ -8,23 +8,34 @@
|
|||||||
|
|
||||||
#include "core/entities/entity_loader.hpp"
|
#include "core/entities/entity_loader.hpp"
|
||||||
|
|
||||||
BulletConfig BulletRegistry::config;
|
std::unordered_map<std::string, BulletConfig> BulletRegistry::configs;
|
||||||
bool BulletRegistry::loaded = false;
|
bool BulletRegistry::loaded = false;
|
||||||
|
|
||||||
auto BulletRegistry::load() -> bool {
|
namespace {
|
||||||
auto yaml = Entities::EntityLoader::load("bullet");
|
// Tria comú: carrega el YAML d'un name, parseja a BulletConfig i el guarda
|
||||||
|
// al map. Retorna punter al config inserit, o nullptr si falla.
|
||||||
|
auto loadInto(std::unordered_map<std::string, BulletConfig>& configs, const std::string& name) -> const BulletConfig* {
|
||||||
|
auto yaml = Entities::EntityLoader::load(name);
|
||||||
if (!yaml) {
|
if (!yaml) {
|
||||||
std::cerr << "[BulletRegistry] Error: no s'ha pogut carregar bullet.yaml\n";
|
std::cerr << "[BulletRegistry] Error: no s'ha pogut carregar " << name << ".yaml\n";
|
||||||
return false;
|
return nullptr;
|
||||||
}
|
}
|
||||||
auto cfg = BulletConfig::fromYaml(*yaml);
|
auto cfg = BulletConfig::fromYaml(*yaml);
|
||||||
if (!cfg) {
|
if (!cfg) {
|
||||||
std::cerr << "[BulletRegistry] Error: format invàlid a bullet.yaml\n";
|
std::cerr << "[BulletRegistry] Error: format invàlid a " << name << ".yaml\n";
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
auto [it, _] = configs.insert_or_assign(name, *cfg);
|
||||||
|
return &it->second;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
auto BulletRegistry::load() -> bool {
|
||||||
|
if (loadInto(configs, "bullet") == nullptr) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
config = *cfg;
|
|
||||||
loaded = true;
|
loaded = true;
|
||||||
std::cout << "[BulletRegistry] Configuració de bala carregada.\n";
|
std::cout << "[BulletRegistry] Configuració de bala 'bullet' carregada.\n";
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,5 +44,13 @@ auto BulletRegistry::get() -> const BulletConfig& {
|
|||||||
std::cerr << "[BulletRegistry] FATAL: get() abans de load()\n";
|
std::cerr << "[BulletRegistry] FATAL: get() abans de load()\n";
|
||||||
std::exit(EXIT_FAILURE);
|
std::exit(EXIT_FAILURE);
|
||||||
}
|
}
|
||||||
return config;
|
return configs.at("bullet");
|
||||||
|
}
|
||||||
|
|
||||||
|
auto BulletRegistry::get(const std::string& name) -> const BulletConfig* {
|
||||||
|
auto it = configs.find(name);
|
||||||
|
if (it != configs.end()) {
|
||||||
|
return &it->second;
|
||||||
|
}
|
||||||
|
return loadInto(configs, name);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,34 @@
|
|||||||
// bullet_registry.hpp - Registre estàtic de la configuració de la bala
|
// bullet_registry.hpp - Registre estàtic de configuracions de bala
|
||||||
// © 2026 JailDesigner
|
// © 2026 JailDesigner
|
||||||
//
|
//
|
||||||
// Una única instància per a tota la sessió. Es manté el patró registry
|
// Diverses configuracions per nom (data/entities/<name>/<name>.yaml). El
|
||||||
// (paral·lel a EnemyRegistry) tot i ser una sola entitat: si el dia de demà
|
// config "bullet" es manté com a default històric (player) i es carrega via
|
||||||
// hi ha més tipus de bala (laser/plasma/etc.) només cal estendre-ho.
|
// load(). Els altres (ex. "bullet_long" per a bales d'enemic) es carreguen
|
||||||
|
// peresosament la primera vegada que algú els demana per nom.
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
#include "game/entities/bullet_config.hpp"
|
#include "game/entities/bullet_config.hpp"
|
||||||
|
|
||||||
class BulletRegistry {
|
class BulletRegistry {
|
||||||
public:
|
public:
|
||||||
BulletRegistry() = delete;
|
BulletRegistry() = delete;
|
||||||
|
|
||||||
// Carrega data/entities/bullet/bullet.yaml. Retorna false si falla.
|
// Carrega data/entities/bullet/bullet.yaml com a default. Retorna false si falla.
|
||||||
static auto load() -> bool;
|
static auto load() -> bool;
|
||||||
|
|
||||||
// Accés a la configuració. Avorta amb log fatal si load() no s'ha cridat
|
// Accés a la configuració per defecte ("bullet"). Avorta amb log fatal si
|
||||||
// o ha fallat.
|
// load() no s'ha cridat o ha fallat. Mantingut per a backwards compat del Bullet ctor.
|
||||||
static auto get() -> const BulletConfig&;
|
static auto get() -> const BulletConfig&;
|
||||||
|
|
||||||
|
// Accés a una configuració per nom. Lazy-load: si no està al map, intenta
|
||||||
|
// carregar data/entities/<name>/<name>.yaml. Retorna nullptr si no es pot.
|
||||||
|
static auto get(const std::string& name) -> const BulletConfig*;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static BulletConfig config;
|
static std::unordered_map<std::string, BulletConfig> configs;
|
||||||
static bool loaded;
|
static bool loaded;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -28,14 +28,6 @@ namespace {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recupera el "ángulo equivalente" de un body en movimiento (para zigzag).
|
|
||||||
auto velocityToAngle(const Vec2& velocity) -> float {
|
|
||||||
if (velocity.lengthSquared() < 0.0001F) {
|
|
||||||
return 0.0F;
|
|
||||||
}
|
|
||||||
return std::atan2(velocity.y, velocity.x) + (Constants::PI / 2.0F);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Random float [0..1).
|
// Random float [0..1).
|
||||||
auto randFloat01() -> float {
|
auto randFloat01() -> float {
|
||||||
return static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX);
|
return static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX);
|
||||||
@@ -62,9 +54,14 @@ void Enemy::init(EnemyType type, const Vec2* ship_pos) {
|
|||||||
config_ = &EnemyRegistry::get(type);
|
config_ = &EnemyRegistry::get(type);
|
||||||
const EnemyConfig& cfg = *config_;
|
const EnemyConfig& cfg = *config_;
|
||||||
|
|
||||||
if (type_ == EnemyType::SQUARE) {
|
ai_state_ = EnemyAiState{};
|
||||||
tracking_timer_ = 0.0F;
|
ai_state_.tracking_strength = cfg.ai.movement.tracking_strength;
|
||||||
tracking_strength_ = cfg.behavior.tracking_strength;
|
|
||||||
|
// Timers paral·lels a tick: random [0, interval) per evitar que tots els
|
||||||
|
// enemics del mateix tipus disparin sincronitzats al spawn.
|
||||||
|
ai_tick_timers_.resize(cfg.ai.tick.size());
|
||||||
|
for (std::size_t i = 0; i < cfg.ai.tick.size(); ++i) {
|
||||||
|
ai_tick_timers_[i] = randFloat01() * cfg.ai.tick[i].interval;
|
||||||
}
|
}
|
||||||
|
|
||||||
shape_ = Graphics::ShapeLoader::load(cfg.shape.path);
|
shape_ = Graphics::ShapeLoader::load(cfg.shape.path);
|
||||||
@@ -136,7 +133,8 @@ void Enemy::init(EnemyType type, const Vec2* ship_pos) {
|
|||||||
invulnerability_timer_ = cfg.spawn.invulnerability_duration;
|
invulnerability_timer_ = cfg.spawn.invulnerability_duration;
|
||||||
brightness_ = cfg.spawn.invulnerability_brightness_start;
|
brightness_ = cfg.spawn.invulnerability_brightness_start;
|
||||||
|
|
||||||
direction_change_timer_ = 0.0F;
|
health_ = cfg.health;
|
||||||
|
flash_timer_ = 0.0F;
|
||||||
|
|
||||||
is_active_ = true;
|
is_active_ = true;
|
||||||
}
|
}
|
||||||
@@ -155,6 +153,11 @@ void Enemy::update(float delta_time) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (flash_timer_ > 0.0F) {
|
||||||
|
flash_timer_ -= delta_time;
|
||||||
|
flash_timer_ = std::max(flash_timer_, 0.0F);
|
||||||
|
}
|
||||||
|
|
||||||
if (invulnerability_timer_ > 0.0F) {
|
if (invulnerability_timer_ > 0.0F) {
|
||||||
invulnerability_timer_ -= delta_time;
|
invulnerability_timer_ -= delta_time;
|
||||||
invulnerability_timer_ = std::max(invulnerability_timer_, 0.0F);
|
invulnerability_timer_ = std::max(invulnerability_timer_, 0.0F);
|
||||||
@@ -167,22 +170,9 @@ void Enemy::update(float delta_time) {
|
|||||||
brightness_ = START + ((END - START) * SMOOTH_T);
|
brightness_ = START + ((END - START) * SMOOTH_T);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isWounded()) {
|
// El moviment es delega a Systems::EnemyAi::tick, invocat des de l'scene
|
||||||
switch (type_) {
|
// ABANS d'aquest update (manté l'ordre: AI escriu velocity/rotation_delta,
|
||||||
case EnemyType::PENTAGON:
|
// després animation pot modular rotation_delta via rotation_accel).
|
||||||
case EnemyType::STAR:
|
|
||||||
// STAR reusa el zigzag esquivador de Pentagon. Si en el futur
|
|
||||||
// vol comportament propi, separa-li el cas.
|
|
||||||
behaviorPentagon(delta_time);
|
|
||||||
break;
|
|
||||||
case EnemyType::SQUARE:
|
|
||||||
behaviorSquare(delta_time);
|
|
||||||
break;
|
|
||||||
case EnemyType::PINWHEEL:
|
|
||||||
behaviorPinwheel(delta_time);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateAnimation(delta_time);
|
updateAnimation(delta_time);
|
||||||
|
|
||||||
@@ -210,7 +200,15 @@ void Enemy::draw() const {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rendering::renderShape(renderer_, shape_, center_, rotation_, SCALE, 1.0F, brightness_, color);
|
// Flash d'impacte parcial (HP>1): força el color a blanc i el brillo a
|
||||||
|
// 1.0 durant la finestra de flash. Té prioritat sobre el blink wounded.
|
||||||
|
float effective_brightness = brightness_;
|
||||||
|
if (flash_timer_ > 0.0F) {
|
||||||
|
color = SDL_Color{.r = 255, .g = 255, .b = 255, .a = 255};
|
||||||
|
effective_brightness = 1.0F;
|
||||||
|
}
|
||||||
|
|
||||||
|
Rendering::renderShape(renderer_, shape_, center_, rotation_, SCALE, 1.0F, effective_brightness, color);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Enemy::destroy() {
|
void Enemy::destroy() {
|
||||||
@@ -221,6 +219,7 @@ void Enemy::destroy() {
|
|||||||
wounded_timer_ = 0.0F;
|
wounded_timer_ = 0.0F;
|
||||||
wound_expired_this_frame_ = false;
|
wound_expired_this_frame_ = false;
|
||||||
last_hit_by_ = 0xFF;
|
last_hit_by_ = 0xFF;
|
||||||
|
flash_timer_ = 0.0F;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Enemy::hurt(uint8_t shooter_id) {
|
void Enemy::hurt(uint8_t shooter_id) {
|
||||||
@@ -246,59 +245,6 @@ void Enemy::setVelocityFromAngle(float angle_movement, float speed) {
|
|||||||
body_.velocity = angleToDirection(angle_movement) * speed;
|
body_.velocity = angleToDirection(angle_movement) * speed;
|
||||||
}
|
}
|
||||||
|
|
||||||
// PENTAGON: zigzag esquivador. Canvis de direcció periòdics (probabilístics)
|
|
||||||
// en lloc de detectar parets; el rebot contra murs el fa PhysicsWorld.
|
|
||||||
void Enemy::behaviorPentagon(float delta_time) {
|
|
||||||
direction_change_timer_ += delta_time;
|
|
||||||
|
|
||||||
if (randFloat01() < config_->behavior.zigzag_prob_per_second * delta_time) {
|
|
||||||
const float CURRENT_ANGLE = velocityToAngle(body_.velocity);
|
|
||||||
const float DELTA = randFloat01() * config_->behavior.angle_change_max;
|
|
||||||
const float NEW_ANGLE = CURRENT_ANGLE + ((std::rand() % 2 == 0) ? DELTA : -DELTA);
|
|
||||||
const float SPEED = body_.velocity.length();
|
|
||||||
setVelocityFromAngle(NEW_ANGLE, SPEED);
|
|
||||||
direction_change_timer_ = 0.0F;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SQUARE: tracking discret cap a la nau cada N segons.
|
|
||||||
void Enemy::behaviorSquare(float delta_time) {
|
|
||||||
tracking_timer_ += delta_time;
|
|
||||||
|
|
||||||
if (tracking_timer_ >= config_->behavior.tracking_interval && ship_position_ != nullptr) {
|
|
||||||
tracking_timer_ = 0.0F;
|
|
||||||
|
|
||||||
const Vec2 TO_SHIP = *ship_position_ - center_;
|
|
||||||
const float DIST = TO_SHIP.length();
|
|
||||||
if (DIST > 0.0F) {
|
|
||||||
const Vec2 DESIRED_DIR = TO_SHIP / DIST;
|
|
||||||
const float SPEED = body_.velocity.length();
|
|
||||||
const Vec2 DESIRED_VEL = DESIRED_DIR * SPEED;
|
|
||||||
|
|
||||||
body_.velocity = (body_.velocity * (1.0F - tracking_strength_)) +
|
|
||||||
(DESIRED_VEL * tracking_strength_);
|
|
||||||
|
|
||||||
const float NEW_SPEED = body_.velocity.length();
|
|
||||||
if (NEW_SPEED > 0.0F) {
|
|
||||||
body_.velocity = body_.velocity * (SPEED / NEW_SPEED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PINWHEEL: movement rectilini + boost de rotació visual prop del ship.
|
|
||||||
void Enemy::behaviorPinwheel(float /*delta_time*/) {
|
|
||||||
if (ship_position_ != nullptr) {
|
|
||||||
const Vec2 TO_SHIP = *ship_position_ - center_;
|
|
||||||
const float DIST = TO_SHIP.length();
|
|
||||||
if (DIST < config_->behavior.proximity_distance) {
|
|
||||||
rotation_delta_ = animation_.rotation_delta_base * config_->behavior.rotation_proximity_multiplier;
|
|
||||||
} else {
|
|
||||||
rotation_delta_ = animation_.rotation_delta_base;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Enemy::updateAnimation(float delta_time) {
|
void Enemy::updateAnimation(float delta_time) {
|
||||||
updatePulse(delta_time);
|
updatePulse(delta_time);
|
||||||
updateRotationAcceleration(delta_time);
|
updateRotationAcceleration(delta_time);
|
||||||
@@ -373,7 +319,7 @@ auto Enemy::getBaseRotation() const -> float {
|
|||||||
|
|
||||||
void Enemy::setTrackingStrength(float strength) {
|
void Enemy::setTrackingStrength(float strength) {
|
||||||
if (type_ == EnemyType::SQUARE) {
|
if (type_ == EnemyType::SQUARE) {
|
||||||
tracking_strength_ = strength;
|
ai_state_.tracking_strength = strength;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,17 +4,24 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <array>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "core/defaults/enemies.hpp"
|
||||||
#include "core/entities/entity.hpp"
|
#include "core/entities/entity.hpp"
|
||||||
#include "core/types.hpp"
|
#include "core/types.hpp"
|
||||||
|
#include "game/entities/enemy_ai.hpp"
|
||||||
|
|
||||||
|
class Ship;
|
||||||
|
|
||||||
// Tipo de enemy
|
// Tipo de enemy
|
||||||
enum class EnemyType : uint8_t {
|
enum class EnemyType : uint8_t {
|
||||||
PENTAGON = 0, // Pentágono esquivador (zigzag)
|
PENTAGON = 0, // Pentágono esquivador (zigzag)
|
||||||
SQUARE = 1, // Square perseguidor (tracks ship)
|
SQUARE = 1, // Square perseguidor (tracks ship)
|
||||||
PINWHEEL = 2, // Molinillo agresivo (rápido, girando)
|
PINWHEEL = 2, // Molinillo agresivo (rápido, girando)
|
||||||
STAR = 3 // Estrella de 5 puntes (clone visual de Pentagon, comportament zigzag)
|
STAR = 3, // Estrella de 5 puntes (clone visual de Pentagon, comportament zigzag)
|
||||||
|
ORB = 4, // Orb gegant tough (HP=10, chase lent — primer enemic HP>1)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Forward declaration — EnemyConfig viu a enemy_config.hpp i s'inclou només a enemy.cpp.
|
// Forward declaration — EnemyConfig viu a enemy_config.hpp i s'inclou només a enemy.cpp.
|
||||||
@@ -70,8 +77,18 @@ class Enemy : public Entities::Entity {
|
|||||||
// ha estat inicialitzat almenys un cop; abans és nullptr.
|
// ha estat inicialitzat almenys un cop; abans és nullptr.
|
||||||
[[nodiscard]] auto getConfig() const -> const EnemyConfig& { return *config_; }
|
[[nodiscard]] auto getConfig() const -> const EnemyConfig& { return *config_; }
|
||||||
|
|
||||||
// Set ship position reference for tracking behavior
|
// Referències als 2 ships per a AI de tracking/proximity/chase/flee.
|
||||||
void setShipPosition(const Vec2* ship_pos) { ship_position_ = ship_pos; }
|
// nullptr = ship inexistent al match. El sistema d'IA filtra per ship->isActive().
|
||||||
|
void setShips(const Ship* p1, const Ship* p2) { ships_ = {p1, p2}; }
|
||||||
|
[[nodiscard]] auto getShips() const -> const std::array<const Ship*, 2>& { return ships_; }
|
||||||
|
|
||||||
|
// Accessors per al sistema d'IA (Systems::EnemyAi).
|
||||||
|
[[nodiscard]] auto getAiState() -> EnemyAiState& { return ai_state_; }
|
||||||
|
[[nodiscard]] auto getAiTickTimers() -> std::vector<float>& { return ai_tick_timers_; }
|
||||||
|
[[nodiscard]] auto getRotationBase() const -> float { return animation_.rotation_delta_base; }
|
||||||
|
void setRotationDelta(float rot) { rotation_delta_ = rot; }
|
||||||
|
// Public: el sistema d'IA reorienta la velocitat des d'un angle.
|
||||||
|
void setVelocityFromAngle(float angle_movement, float speed);
|
||||||
|
|
||||||
// Stage system API (base stats)
|
// Stage system API (base stats)
|
||||||
[[nodiscard]] auto getBaseVelocity() const -> float;
|
[[nodiscard]] auto getBaseVelocity() const -> float;
|
||||||
@@ -101,6 +118,20 @@ class Enemy : public Entities::Entity {
|
|||||||
void consumeWoundExpired() { wound_expired_this_frame_ = false; }
|
void consumeWoundExpired() { wound_expired_this_frame_ = false; }
|
||||||
[[nodiscard]] auto getLastHitBy() const -> uint8_t { return last_hit_by_; }
|
[[nodiscard]] auto getLastHitBy() const -> uint8_t { return last_hit_by_; }
|
||||||
|
|
||||||
|
// Salut: decrementada per l'acció DECREASE_HEALTH al dispatcher d'events.
|
||||||
|
// Quan arriba a 0 o menys, el dispatcher dispara ON_NO_HEALTH (que
|
||||||
|
// típicament encadena SET_HURT o DESTROY al YAML). last_hit_by s'actualitza
|
||||||
|
// al decrement perquè la mort posterior atribueixi correctament el kill.
|
||||||
|
[[nodiscard]] auto getHealth() const -> int { return health_; }
|
||||||
|
void decrementHealth(uint8_t shooter_id = 0xFF) {
|
||||||
|
--health_;
|
||||||
|
last_hit_by_ = shooter_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flash visual: brief impacto-feedback quan rep un hit no letal (HP>1).
|
||||||
|
// Disparat per l'acció FLASH; el render alça la lluminositat mentre dura.
|
||||||
|
void triggerFlash() { flash_timer_ = Defaults::Enemies::Visual::FLASH_DURATION; }
|
||||||
|
|
||||||
// Aplica un impulso (cambio inmediato de velocidad mass-aware) al cuerpo físico.
|
// Aplica un impulso (cambio inmediato de velocidad mass-aware) al cuerpo físico.
|
||||||
void applyImpulse(const Vec2& impulse);
|
void applyImpulse(const Vec2& impulse);
|
||||||
|
|
||||||
@@ -120,11 +151,16 @@ class Enemy : public Entities::Entity {
|
|||||||
EnemyType type_{EnemyType::PENTAGON};
|
EnemyType type_{EnemyType::PENTAGON};
|
||||||
EnemyAnimation animation_;
|
EnemyAnimation animation_;
|
||||||
|
|
||||||
// Comportamiento type-specific
|
// Estat per-instància que la primitiva de moviment manté entre frames.
|
||||||
float tracking_timer_{0.0F};
|
EnemyAiState ai_state_;
|
||||||
const Vec2* ship_position_{nullptr};
|
|
||||||
float tracking_strength_{0.0F};
|
// Timers paral·lels a config_->ai.tick: timers_[i] és el temps restant
|
||||||
float direction_change_timer_{0.0F};
|
// (en segons) fins a la pròxima execució de l'acció i. Re-dimensionat a
|
||||||
|
// init() segons la mida de config_->ai.tick.
|
||||||
|
std::vector<float> ai_tick_timers_;
|
||||||
|
|
||||||
|
// Referències als 2 ships per a AI de tracking/proximity/chase/flee.
|
||||||
|
std::array<const Ship*, 2> ships_{nullptr, nullptr};
|
||||||
|
|
||||||
// Invulnerabilidad post-spawn
|
// Invulnerabilidad post-spawn
|
||||||
float invulnerability_timer_{0.0F};
|
float invulnerability_timer_{0.0F};
|
||||||
@@ -134,18 +170,21 @@ class Enemy : public Entities::Entity {
|
|||||||
bool wound_expired_this_frame_{false};
|
bool wound_expired_this_frame_{false};
|
||||||
uint8_t last_hit_by_{0xFF};
|
uint8_t last_hit_by_{0xFF};
|
||||||
|
|
||||||
|
// Salut per-instància. Reseteja a config_->health a init(); el dispatcher
|
||||||
|
// d'events la decrementa via DECREASE_HEALTH i dispara ON_NO_HEALTH quan
|
||||||
|
// creua zero. Permet enemics tough (HP>1) sense canvis al motor.
|
||||||
|
int health_{1};
|
||||||
|
|
||||||
|
// Flash visual temporitzat per a feedback d'impacte parcial (HP>1).
|
||||||
|
// L'acció FLASH el reseteja a FLASH_DURATION; draw() alça la lluminositat
|
||||||
|
// mentre dura, i update() el decrementa.
|
||||||
|
float flash_timer_{0.0F};
|
||||||
|
|
||||||
// Métodos privados
|
// Métodos privados
|
||||||
void updateAnimation(float delta_time);
|
void updateAnimation(float delta_time);
|
||||||
void updatePulse(float delta_time);
|
void updatePulse(float delta_time);
|
||||||
void updateRotationAcceleration(float delta_time);
|
void updateRotationAcceleration(float delta_time);
|
||||||
void behaviorPentagon(float delta_time);
|
|
||||||
void behaviorSquare(float delta_time);
|
|
||||||
void behaviorPinwheel(float delta_time);
|
|
||||||
[[nodiscard]] auto computeCurrentScale() const -> float;
|
[[nodiscard]] auto computeCurrentScale() const -> float;
|
||||||
// Static: passa els paràmetres com a args per no acoblar a *this.
|
// Static: passa els paràmetres com a args per no acoblar a *this.
|
||||||
static auto attemptSafeSpawn(const Vec2& ship_pos, float collision_radius, float safety_distance, float& out_x, float& out_y) -> bool;
|
static auto attemptSafeSpawn(const Vec2& ship_pos, float collision_radius, float safety_distance, float& out_x, float& out_y) -> bool;
|
||||||
|
|
||||||
// Helper: setear body_.velocity desde un ángulo y magnitud.
|
|
||||||
// angle_movement=0 apunta hacia arriba (eje Y negativo SDL).
|
|
||||||
void setVelocityFromAngle(float angle_movement, float speed);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
// enemy_ai.hpp - Sistema declaratiu d'IA per a enemics
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
//
|
||||||
|
// Cada enemic declara al seu YAML quin movement primitiu fa servir i, opcional-
|
||||||
|
// ment, una llista d'accions periòdiques (tick). El motor només dispatcha; el
|
||||||
|
// comportament viu a les dades. Patró paral·lel al d'events declaratius
|
||||||
|
// (enemy_event.hpp).
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
// Primitiva de moviment activa per a un enemic. Substitueix el switch
|
||||||
|
// hardcoded sobre EnemyType.
|
||||||
|
enum class MovementType : uint8_t {
|
||||||
|
ZIGZAG, // Canvi de direcció probabilístic agressiu (Pentagon/Star)
|
||||||
|
TRACKING, // LERP discret cap al ship cada N segons (Square)
|
||||||
|
RECTILINEAR_PROXIMITY, // Rectilini + boost rotació visual prop del ship (Pinwheel)
|
||||||
|
WANDER, // Canvi de direcció probabilístic suau, sense target
|
||||||
|
CHASE, // Steering continu cap al ship més proper
|
||||||
|
FLEE, // Steering continu allunyant-se del ship més proper
|
||||||
|
};
|
||||||
|
|
||||||
|
// Accions que s'executen periòdicament (un timer per acció). Futur (Fase C):
|
||||||
|
// SHOOT amb aim_mode/jitter/bullet config.
|
||||||
|
enum class AiActionType : uint8_t {
|
||||||
|
SHOOT,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class AimMode : uint8_t {
|
||||||
|
RANDOM, // Angle uniformement aleatori
|
||||||
|
AIMED, // atan2(nearest_ship - center) + soroll gaussià (jitter_rad)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Camps de tots els movements; només el subset rellevant per al type actiu
|
||||||
|
// s'usa. Els altres queden a 0.0F. Mateixa filosofia que la BehaviorCfg
|
||||||
|
// llegacy però amb el type explícit dins.
|
||||||
|
struct MovementConfig {
|
||||||
|
MovementType type{MovementType::ZIGZAG};
|
||||||
|
|
||||||
|
// ZIGZAG i WANDER (canvi de direcció probabilístic; comparteixen camps).
|
||||||
|
float angle_change_max{0.0F};
|
||||||
|
float zigzag_prob_per_second{0.0F};
|
||||||
|
|
||||||
|
// TRACKING
|
||||||
|
float tracking_strength{0.0F};
|
||||||
|
float tracking_interval{0.0F};
|
||||||
|
|
||||||
|
// RECTILINEAR_PROXIMITY
|
||||||
|
float rotation_proximity_multiplier{0.0F};
|
||||||
|
float proximity_distance{0.0F};
|
||||||
|
|
||||||
|
// CHASE / FLEE: força del steering per segon (LERP velocity ↔ direcció ideal).
|
||||||
|
// 1.0 = en ~1s la velocitat queda totalment realineada cap al target.
|
||||||
|
float chase_strength{0.0F};
|
||||||
|
float flee_strength{0.0F};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Acció periòdica. interval = segons entre disparades; el dispatcher manté un
|
||||||
|
// timer per acció (paral·lel a aquesta llista) i dispara quan arriba a 0.
|
||||||
|
struct AiTickAction {
|
||||||
|
AiActionType type{AiActionType::SHOOT};
|
||||||
|
float interval{1.0F};
|
||||||
|
AimMode aim_mode{AimMode::RANDOM};
|
||||||
|
float jitter_rad{0.0F};
|
||||||
|
float bullet_speed{200.0F}; // px/s; la magnitud la decideix l'enemic, no el bullet config
|
||||||
|
std::string bullet_config_name; // bullet config a usar (lazy-load des de BulletRegistry)
|
||||||
|
};
|
||||||
|
|
||||||
|
struct EnemyAiConfig {
|
||||||
|
MovementConfig movement;
|
||||||
|
std::vector<AiTickAction> tick;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Estat per-instància que la primitiva de moviment manté entre frames (timers
|
||||||
|
// d'interval, contadors de canvi de direcció...). Es viu dins de Enemy i el
|
||||||
|
// sistema d'IA hi escriu via getAiState().
|
||||||
|
struct EnemyAiState {
|
||||||
|
float direction_change_timer{0.0F}; // ZIGZAG
|
||||||
|
float tracking_timer{0.0F}; // TRACKING
|
||||||
|
float tracking_strength{0.0F}; // TRACKING (cau de cfg, mutable per dificultat)
|
||||||
|
};
|
||||||
@@ -30,6 +30,7 @@ namespace {
|
|||||||
if (s == "square") { return EnemyType::SQUARE; }
|
if (s == "square") { return EnemyType::SQUARE; }
|
||||||
if (s == "pinwheel") { return EnemyType::PINWHEEL; }
|
if (s == "pinwheel") { return EnemyType::PINWHEEL; }
|
||||||
if (s == "star") { return EnemyType::STAR; }
|
if (s == "star") { return EnemyType::STAR; }
|
||||||
|
if (s == "orb") { return EnemyType::ORB; }
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,13 +178,31 @@ namespace {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// health és opcional: si el YAML no l'inclou, el default {1} de l'struct
|
||||||
|
// ja cobreix el comportament de tots els enemics actuals (1 hit → mort).
|
||||||
|
void parseHealth(const fkyaml::node& node, int& out) {
|
||||||
|
if (node.contains("health")) {
|
||||||
|
out = node["health"].get_value<int>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forward-decl: aimModeFromString viu més avall (junt amb la resta de
|
||||||
|
// helpers d'AI) però parseActionList el necessita per al payload de
|
||||||
|
// FIRE_BULLET. Evita reordenar tot el bloc.
|
||||||
|
auto aimModeFromString(const std::string& s) -> std::optional<AimMode>;
|
||||||
|
|
||||||
auto actionTypeFromString(const std::string& s) -> std::optional<EnemyActionType> {
|
auto actionTypeFromString(const std::string& s) -> std::optional<EnemyActionType> {
|
||||||
if (s == "set_hurt") { return EnemyActionType::SET_HURT; }
|
if (s == "set_hurt") { return EnemyActionType::SET_HURT; }
|
||||||
if (s == "destroy") { return EnemyActionType::DESTROY; }
|
if (s == "destroy") { return EnemyActionType::DESTROY; }
|
||||||
if (s == "add_score") { return EnemyActionType::ADD_SCORE; }
|
if (s == "add_score") { return EnemyActionType::ADD_SCORE; }
|
||||||
if (s == "create_debris") { return EnemyActionType::CREATE_DEBRIS; }
|
if (s == "create_debris") { return EnemyActionType::CREATE_DEBRIS; }
|
||||||
|
if (s == "create_debris_partial") { return EnemyActionType::CREATE_DEBRIS_PARTIAL; }
|
||||||
if (s == "create_fireworks") { return EnemyActionType::CREATE_FIREWORKS; }
|
if (s == "create_fireworks") { return EnemyActionType::CREATE_FIREWORKS; }
|
||||||
|
if (s == "create_fireworks_small") { return EnemyActionType::CREATE_FIREWORKS_SMALL; }
|
||||||
if (s == "apply_impulse") { return EnemyActionType::APPLY_IMPULSE; }
|
if (s == "apply_impulse") { return EnemyActionType::APPLY_IMPULSE; }
|
||||||
|
if (s == "decrease_health") { return EnemyActionType::DECREASE_HEALTH; }
|
||||||
|
if (s == "flash") { return EnemyActionType::FLASH; }
|
||||||
|
if (s == "fire_bullet") { return EnemyActionType::FIRE_BULLET; }
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,20 +225,198 @@ namespace {
|
|||||||
<< event_name << " (" << enemy_name << ")\n";
|
<< event_name << " (" << enemy_name << ")\n";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
out.push_back({*PARSED});
|
EnemyAction action;
|
||||||
|
action.type = *PARSED;
|
||||||
|
// Payload de FIRE_BULLET. Camps opcionals; els defaults són els del struct.
|
||||||
|
if (action.type == EnemyActionType::FIRE_BULLET) {
|
||||||
|
if (item.contains("bullet")) {
|
||||||
|
action.bullet_config_name = item["bullet"].get_value<std::string>();
|
||||||
|
}
|
||||||
|
if (item.contains("bullet_speed")) {
|
||||||
|
action.bullet_speed = item["bullet_speed"].get_value<float>();
|
||||||
|
}
|
||||||
|
if (item.contains("aim_mode")) {
|
||||||
|
const auto AIM_STR = item["aim_mode"].get_value<std::string>();
|
||||||
|
const auto AIM = aimModeFromString(AIM_STR);
|
||||||
|
if (!AIM) {
|
||||||
|
std::cerr << "[EnemyConfig] Error: aim_mode desconegut '" << AIM_STR
|
||||||
|
<< "' a " << event_name << " (" << enemy_name << ")\n";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
action.aim_mode = *AIM;
|
||||||
|
}
|
||||||
|
if (item.contains("jitter_rad")) {
|
||||||
|
action.jitter_rad = item["jitter_rad"].get_value<float>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.push_back(action);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Defaults: replica el flux hardcoded actual (set_hurt → destroy → score+debris+fireworks).
|
// Defaults: replica el flux hardcoded actual (set_hurt → destroy → score+debris+fireworks).
|
||||||
|
// Construïm via mutació per esquivar warnings de designated-init parcial sobre
|
||||||
|
// l'EnemyAction (que té payload de FIRE_BULLET no rellevant ací).
|
||||||
void fillLegacyDefaults(EnemyEventConfig& events) {
|
void fillLegacyDefaults(EnemyEventConfig& events) {
|
||||||
events.on_hit = {{EnemyActionType::SET_HURT}};
|
const auto MAKE = [](EnemyActionType type) {
|
||||||
events.on_hurt_end = {{EnemyActionType::DESTROY}};
|
EnemyAction a;
|
||||||
events.on_destroy = {
|
a.type = type;
|
||||||
{EnemyActionType::ADD_SCORE},
|
return a;
|
||||||
{EnemyActionType::CREATE_DEBRIS},
|
|
||||||
{EnemyActionType::CREATE_FIREWORKS},
|
|
||||||
};
|
};
|
||||||
|
events.on_hit = {MAKE(EnemyActionType::SET_HURT)};
|
||||||
|
events.on_hurt_end = {MAKE(EnemyActionType::DESTROY)};
|
||||||
|
events.on_destroy = {
|
||||||
|
MAKE(EnemyActionType::ADD_SCORE),
|
||||||
|
MAKE(EnemyActionType::CREATE_DEBRIS),
|
||||||
|
MAKE(EnemyActionType::CREATE_FIREWORKS),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto movementTypeFromString(const std::string& s) -> std::optional<MovementType> {
|
||||||
|
if (s == "zigzag") { return MovementType::ZIGZAG; }
|
||||||
|
if (s == "tracking") { return MovementType::TRACKING; }
|
||||||
|
if (s == "rectilinear_proximity") { return MovementType::RECTILINEAR_PROXIMITY; }
|
||||||
|
if (s == "wander") { return MovementType::WANDER; }
|
||||||
|
if (s == "chase") { return MovementType::CHASE; }
|
||||||
|
if (s == "flee") { return MovementType::FLEE; }
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto aiActionTypeFromString(const std::string& s) -> std::optional<AiActionType> {
|
||||||
|
if (s == "shoot") { return AiActionType::SHOOT; }
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto aimModeFromString(const std::string& s) -> std::optional<AimMode> {
|
||||||
|
if (s == "random") { return AimMode::RANDOM; }
|
||||||
|
if (s == "aimed") { return AimMode::AIMED; }
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto parseMovement(const fkyaml::node& mv_node, const std::string& enemy_name, MovementConfig& out) -> bool {
|
||||||
|
if (!mv_node.contains("type")) {
|
||||||
|
std::cerr << "[EnemyConfig] Error: falta 'ai.movement.type' a " << enemy_name << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const auto TYPE_STR = mv_node["type"].get_value<std::string>();
|
||||||
|
const auto PARSED = movementTypeFromString(TYPE_STR);
|
||||||
|
if (!PARSED) {
|
||||||
|
std::cerr << "[EnemyConfig] Error: movement type desconegut '" << TYPE_STR
|
||||||
|
<< "' a " << enemy_name << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
out.type = *PARSED;
|
||||||
|
const auto READ_OPT = [&mv_node](const char* key, float& dst) {
|
||||||
|
if (mv_node.contains(key)) {
|
||||||
|
dst = mv_node[key].get_value<float>();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
READ_OPT("angle_change_max", out.angle_change_max);
|
||||||
|
READ_OPT("zigzag_prob_per_second", out.zigzag_prob_per_second);
|
||||||
|
READ_OPT("tracking_strength", out.tracking_strength);
|
||||||
|
READ_OPT("tracking_interval", out.tracking_interval);
|
||||||
|
READ_OPT("rotation_proximity_multiplier", out.rotation_proximity_multiplier);
|
||||||
|
READ_OPT("proximity_distance", out.proximity_distance);
|
||||||
|
READ_OPT("chase_strength", out.chase_strength);
|
||||||
|
READ_OPT("flee_strength", out.flee_strength);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto parseTickList(const fkyaml::node& list_node, const std::string& enemy_name, std::vector<AiTickAction>& out) -> bool {
|
||||||
|
if (!list_node.is_sequence()) {
|
||||||
|
std::cerr << "[EnemyConfig] Error: 'ai.tick' ha de ser una llista a " << enemy_name << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (const auto& item : list_node) {
|
||||||
|
if (!item.contains("action")) {
|
||||||
|
std::cerr << "[EnemyConfig] Error: entrada sense 'action' a ai.tick ("
|
||||||
|
<< enemy_name << ")\n";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const auto STR = item["action"].get_value<std::string>();
|
||||||
|
const auto PARSED = aiActionTypeFromString(STR);
|
||||||
|
if (!PARSED) {
|
||||||
|
std::cerr << "[EnemyConfig] Error: acció d'ai desconeguda '" << STR
|
||||||
|
<< "' a " << enemy_name << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
AiTickAction action;
|
||||||
|
action.type = *PARSED;
|
||||||
|
if (item.contains("interval")) {
|
||||||
|
action.interval = item["interval"].get_value<float>();
|
||||||
|
}
|
||||||
|
if (item.contains("aim_mode")) {
|
||||||
|
const auto AIM_STR = item["aim_mode"].get_value<std::string>();
|
||||||
|
const auto AIM = aimModeFromString(AIM_STR);
|
||||||
|
if (!AIM) {
|
||||||
|
std::cerr << "[EnemyConfig] Error: aim_mode desconegut '" << AIM_STR
|
||||||
|
<< "' a ai.tick (" << enemy_name << ")\n";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
action.aim_mode = *AIM;
|
||||||
|
}
|
||||||
|
if (item.contains("jitter_rad")) {
|
||||||
|
action.jitter_rad = item["jitter_rad"].get_value<float>();
|
||||||
|
}
|
||||||
|
if (item.contains("bullet_speed")) {
|
||||||
|
action.bullet_speed = item["bullet_speed"].get_value<float>();
|
||||||
|
}
|
||||||
|
if (item.contains("bullet")) {
|
||||||
|
action.bullet_config_name = item["bullet"].get_value<std::string>();
|
||||||
|
}
|
||||||
|
out.push_back(action);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migració progressiva: si el YAML no porta secció `ai:`, derivem el
|
||||||
|
// movement a partir de l'ai_type i copiem els paràmetres de la BehaviorCfg
|
||||||
|
// ja parsejada. Comportament idèntic al hardcoded actual.
|
||||||
|
void fillLegacyAiDefaults(EnemyType ai_type, const EnemyConfig::BehaviorCfg& legacy, EnemyAiConfig& out) {
|
||||||
|
switch (ai_type) {
|
||||||
|
case EnemyType::PENTAGON:
|
||||||
|
case EnemyType::STAR:
|
||||||
|
out.movement.type = MovementType::ZIGZAG;
|
||||||
|
out.movement.angle_change_max = legacy.angle_change_max;
|
||||||
|
out.movement.zigzag_prob_per_second = legacy.zigzag_prob_per_second;
|
||||||
|
break;
|
||||||
|
case EnemyType::SQUARE:
|
||||||
|
out.movement.type = MovementType::TRACKING;
|
||||||
|
out.movement.tracking_strength = legacy.tracking_strength;
|
||||||
|
out.movement.tracking_interval = legacy.tracking_interval;
|
||||||
|
break;
|
||||||
|
case EnemyType::PINWHEEL:
|
||||||
|
out.movement.type = MovementType::RECTILINEAR_PROXIMITY;
|
||||||
|
out.movement.rotation_proximity_multiplier = legacy.rotation_proximity_multiplier;
|
||||||
|
out.movement.proximity_distance = legacy.proximity_distance;
|
||||||
|
break;
|
||||||
|
case EnemyType::ORB:
|
||||||
|
// Sense legacy fallback: el YAML de l'orb ha de definir
|
||||||
|
// ai.movement explícitament. Default chase lent perquè el switch
|
||||||
|
// siga exhaustiu i no falli si algú omet el bloc ai.
|
||||||
|
out.movement.type = MovementType::CHASE;
|
||||||
|
out.movement.chase_strength = 0.3F;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto parseAi(const fkyaml::node& node, const std::string& name, EnemyType ai_type, const EnemyConfig::BehaviorCfg& legacy, EnemyAiConfig& out) -> bool {
|
||||||
|
if (!node.contains("ai")) {
|
||||||
|
fillLegacyAiDefaults(ai_type, legacy, out);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const auto& ai = node["ai"];
|
||||||
|
if (!ai.contains("movement")) {
|
||||||
|
std::cerr << "[EnemyConfig] Error: falta 'ai.movement' a " << name << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!parseMovement(ai["movement"], name, out.movement)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (ai.contains("tick") && !parseTickList(ai["tick"], name, out.tick)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto parseEvents(const fkyaml::node& node, const std::string& name, EnemyEventConfig& out) -> bool {
|
auto parseEvents(const fkyaml::node& node, const std::string& name, EnemyEventConfig& out) -> bool {
|
||||||
@@ -231,6 +428,10 @@ namespace {
|
|||||||
if (e.contains("on_hit") && !parseActionList(e["on_hit"], name, "on_hit", out.on_hit)) {
|
if (e.contains("on_hit") && !parseActionList(e["on_hit"], name, "on_hit", out.on_hit)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (e.contains("on_no_health") &&
|
||||||
|
!parseActionList(e["on_no_health"], name, "on_no_health", out.on_no_health)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (e.contains("on_hurt_end") &&
|
if (e.contains("on_hurt_end") &&
|
||||||
!parseActionList(e["on_hurt_end"], name, "on_hurt_end", out.on_hurt_end)) {
|
!parseActionList(e["on_hurt_end"], name, "on_hurt_end", out.on_hurt_end)) {
|
||||||
return false;
|
return false;
|
||||||
@@ -267,7 +468,9 @@ auto EnemyConfig::fromYaml(const fkyaml::node& node, EnemyType expected_ai_type)
|
|||||||
if (!parseSpawn(node, cfg.name, cfg.spawn)) { return std::nullopt; }
|
if (!parseSpawn(node, cfg.name, cfg.spawn)) { return std::nullopt; }
|
||||||
if (!parseColors(node, cfg.name, cfg.colors)) { return std::nullopt; }
|
if (!parseColors(node, cfg.name, cfg.colors)) { return std::nullopt; }
|
||||||
if (!parseScore(node, cfg.name, cfg.score)) { return std::nullopt; }
|
if (!parseScore(node, cfg.name, cfg.score)) { return std::nullopt; }
|
||||||
|
parseHealth(node, cfg.health);
|
||||||
if (!parseEvents(node, cfg.name, cfg.events)) { return std::nullopt; }
|
if (!parseEvents(node, cfg.name, cfg.events)) { return std::nullopt; }
|
||||||
|
if (!parseAi(node, cfg.name, cfg.ai_type, cfg.behavior, cfg.ai)) { return std::nullopt; }
|
||||||
|
|
||||||
return cfg;
|
return cfg;
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
#include "external/fkyaml_node.hpp"
|
#include "external/fkyaml_node.hpp"
|
||||||
#include "game/entities/enemy.hpp" // EnemyType
|
#include "game/entities/enemy.hpp" // EnemyType
|
||||||
|
#include "game/entities/enemy_ai.hpp"
|
||||||
#include "game/entities/enemy_event.hpp"
|
#include "game/entities/enemy_event.hpp"
|
||||||
|
|
||||||
struct EnemyConfig {
|
struct EnemyConfig {
|
||||||
@@ -99,7 +100,12 @@ struct EnemyConfig {
|
|||||||
SpawnCfg spawn;
|
SpawnCfg spawn;
|
||||||
ColorsCfg colors;
|
ColorsCfg colors;
|
||||||
int score;
|
int score;
|
||||||
|
// Salut inicial: per defecte 1 (un balazo → on_no_health). Els YAMLs poden
|
||||||
|
// pujar-lo (p.ex. 10 per a un enemic tough). El sistema d'events és qui
|
||||||
|
// decideix què passa quan la salut arriba a 0 via on_no_health.
|
||||||
|
int health{1};
|
||||||
EnemyEventConfig events;
|
EnemyEventConfig events;
|
||||||
|
EnemyAiConfig ai;
|
||||||
|
|
||||||
// Parseja un descriptor d'enemic. expected_ai_type valida que ai_type del
|
// Parseja un descriptor d'enemic. expected_ai_type valida que ai_type del
|
||||||
// YAML coincideix amb el tipus que el caller espera (segons el directori).
|
// YAML coincideix amb el tipus que el caller espera (segons el directori).
|
||||||
|
|||||||
@@ -8,10 +8,14 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "game/entities/enemy_ai.hpp" // AimMode
|
||||||
|
|
||||||
enum class EnemyEventType : uint8_t {
|
enum class EnemyEventType : uint8_t {
|
||||||
ON_HIT, // Impactat per una bala
|
ON_HIT, // Impactat per una bala
|
||||||
|
ON_NO_HEALTH, // health ha arribat a 0 o menys aquest frame (via DECREASE_HEALTH)
|
||||||
ON_HURT_END, // Timer wounded ha expirat aquest frame
|
ON_HURT_END, // Timer wounded ha expirat aquest frame
|
||||||
ON_DESTROY, // L'acció destroy s'està executant (efectes col·laterals)
|
ON_DESTROY, // L'acció destroy s'està executant (efectes col·laterals)
|
||||||
};
|
};
|
||||||
@@ -21,16 +25,29 @@ enum class EnemyActionType : uint8_t {
|
|||||||
DESTROY, // Dispara on_destroy + desactiva físicament
|
DESTROY, // Dispara on_destroy + desactiva físicament
|
||||||
ADD_SCORE, // Suma config.score al shooter + floating score
|
ADD_SCORE, // Suma config.score al shooter + floating score
|
||||||
CREATE_DEBRIS, // Explosió de debris amb herència de velocitat
|
CREATE_DEBRIS, // Explosió de debris amb herència de velocitat
|
||||||
|
CREATE_DEBRIS_PARTIAL, // Debris de xip parcial (trossos a escala 0.3, per hits HP>1)
|
||||||
CREATE_FIREWORKS, // Burst radial de firework
|
CREATE_FIREWORKS, // Burst radial de firework
|
||||||
|
CREATE_FIREWORKS_SMALL, // Burst petit (pocs punts, poca velocitat) — feedback per hit
|
||||||
APPLY_IMPULSE, // Aplica l'impuls de la bala impactant
|
APPLY_IMPULSE, // Aplica l'impuls de la bala impactant
|
||||||
|
DECREASE_HEALTH, // Decrementa health_; si <=0, dispatcha ON_NO_HEALTH
|
||||||
|
FLASH, // Flash visual breu (feedback per impacte parcial)
|
||||||
|
FIRE_BULLET, // Dispara una bala (config per nom) dirigida o aleatòria
|
||||||
};
|
};
|
||||||
|
|
||||||
struct EnemyAction {
|
struct EnemyAction {
|
||||||
EnemyActionType type;
|
EnemyActionType type;
|
||||||
|
|
||||||
|
// Payload de FIRE_BULLET (ignorat per altres tipus). Paral·lel a AiTickAction
|
||||||
|
// perquè un futur refactor pugui compartir doShoot/doFireBullet si val la pena.
|
||||||
|
std::string bullet_config_name;
|
||||||
|
float bullet_speed{200.0F};
|
||||||
|
AimMode aim_mode{AimMode::RANDOM};
|
||||||
|
float jitter_rad{0.0F};
|
||||||
};
|
};
|
||||||
|
|
||||||
struct EnemyEventConfig {
|
struct EnemyEventConfig {
|
||||||
std::vector<EnemyAction> on_hit;
|
std::vector<EnemyAction> on_hit;
|
||||||
|
std::vector<EnemyAction> on_no_health;
|
||||||
std::vector<EnemyAction> on_hurt_end;
|
std::vector<EnemyAction> on_hurt_end;
|
||||||
std::vector<EnemyAction> on_destroy;
|
std::vector<EnemyAction> on_destroy;
|
||||||
|
|
||||||
@@ -38,6 +55,8 @@ struct EnemyEventConfig {
|
|||||||
switch (event) {
|
switch (event) {
|
||||||
case EnemyEventType::ON_HIT:
|
case EnemyEventType::ON_HIT:
|
||||||
return on_hit;
|
return on_hit;
|
||||||
|
case EnemyEventType::ON_NO_HEALTH:
|
||||||
|
return on_no_health;
|
||||||
case EnemyEventType::ON_HURT_END:
|
case EnemyEventType::ON_HURT_END:
|
||||||
return on_hurt_end;
|
return on_hurt_end;
|
||||||
case EnemyEventType::ON_DESTROY:
|
case EnemyEventType::ON_DESTROY:
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ EnemyConfig EnemyRegistry::pentagon_config;
|
|||||||
EnemyConfig EnemyRegistry::square_config;
|
EnemyConfig EnemyRegistry::square_config;
|
||||||
EnemyConfig EnemyRegistry::pinwheel_config;
|
EnemyConfig EnemyRegistry::pinwheel_config;
|
||||||
EnemyConfig EnemyRegistry::star_config;
|
EnemyConfig EnemyRegistry::star_config;
|
||||||
|
EnemyConfig EnemyRegistry::orb_config;
|
||||||
bool EnemyRegistry::loaded = false;
|
bool EnemyRegistry::loaded = false;
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -38,10 +39,11 @@ auto EnemyRegistry::loadAll() -> bool {
|
|||||||
const bool OK = loadOne("pentagon", EnemyType::PENTAGON, pentagon_config) &&
|
const bool OK = loadOne("pentagon", EnemyType::PENTAGON, pentagon_config) &&
|
||||||
loadOne("square", EnemyType::SQUARE, square_config) &&
|
loadOne("square", EnemyType::SQUARE, square_config) &&
|
||||||
loadOne("pinwheel", EnemyType::PINWHEEL, pinwheel_config) &&
|
loadOne("pinwheel", EnemyType::PINWHEEL, pinwheel_config) &&
|
||||||
loadOne("star", EnemyType::STAR, star_config);
|
loadOne("star", EnemyType::STAR, star_config) &&
|
||||||
|
loadOne("orb", EnemyType::ORB, orb_config);
|
||||||
loaded = OK;
|
loaded = OK;
|
||||||
if (OK) {
|
if (OK) {
|
||||||
std::cout << "[EnemyRegistry] 4 configuracions d'enemic carregades.\n";
|
std::cout << "[EnemyRegistry] 5 configuracions d'enemic carregades.\n";
|
||||||
}
|
}
|
||||||
return OK;
|
return OK;
|
||||||
}
|
}
|
||||||
@@ -60,6 +62,8 @@ auto EnemyRegistry::get(EnemyType type) -> const EnemyConfig& {
|
|||||||
return pinwheel_config;
|
return pinwheel_config;
|
||||||
case EnemyType::STAR:
|
case EnemyType::STAR:
|
||||||
return star_config;
|
return star_config;
|
||||||
|
case EnemyType::ORB:
|
||||||
|
return orb_config;
|
||||||
}
|
}
|
||||||
std::cerr << "[EnemyRegistry] FATAL: tipus desconegut\n";
|
std::cerr << "[EnemyRegistry] FATAL: tipus desconegut\n";
|
||||||
std::exit(EXIT_FAILURE);
|
std::exit(EXIT_FAILURE);
|
||||||
|
|||||||
@@ -27,5 +27,6 @@ class EnemyRegistry {
|
|||||||
static EnemyConfig square_config;
|
static EnemyConfig square_config;
|
||||||
static EnemyConfig pinwheel_config;
|
static EnemyConfig pinwheel_config;
|
||||||
static EnemyConfig star_config;
|
static EnemyConfig star_config;
|
||||||
|
static EnemyConfig orb_config;
|
||||||
static bool loaded;
|
static bool loaded;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ Ship::Ship(Rendering::Renderer* renderer, PlayerConfig config, const char* shape
|
|||||||
config_(std::move(config)) {
|
config_(std::move(config)) {
|
||||||
brightness_ = Defaults::Brightness::NAU;
|
brightness_ = Defaults::Brightness::NAU;
|
||||||
|
|
||||||
// El shape pot venir del YAML o ser overridden (ex: P2 amb "ship2.shp").
|
// El shape pot venir del YAML o ser overridden (ex: P2 amb "ship/wedge.shp").
|
||||||
const std::string SHAPE_PATH = (shape_override != nullptr) ? shape_override : config_.shape.path;
|
const std::string SHAPE_PATH = (shape_override != nullptr) ? shape_override : config_.shape.path;
|
||||||
shape_ = Graphics::ShapeLoader::load(SHAPE_PATH);
|
shape_ = Graphics::ShapeLoader::load(SHAPE_PATH);
|
||||||
if (!shape_ || !shape_->isValid()) {
|
if (!shape_ || !shape_->isValid()) {
|
||||||
@@ -85,16 +85,24 @@ void Ship::processInput(float delta_time, uint8_t player_id) {
|
|||||||
? input->checkActionPlayer1(InputAction::THRUST, Input::ALLOW_REPEAT)
|
? input->checkActionPlayer1(InputAction::THRUST, Input::ALLOW_REPEAT)
|
||||||
: input->checkActionPlayer2(InputAction::THRUST, Input::ALLOW_REPEAT);
|
: input->checkActionPlayer2(InputAction::THRUST, Input::ALLOW_REPEAT);
|
||||||
|
|
||||||
if (ROTATE_RIGHT) {
|
applyMovement(ROTATE_LEFT, ROTATE_RIGHT, THRUST, delta_time);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Ship::applyMovement(bool rotate_left, bool rotate_right, bool thrust, float delta_time) {
|
||||||
|
if (is_hit_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rotate_right) {
|
||||||
body_.angle += config_.physics.rotation_speed * delta_time;
|
body_.angle += config_.physics.rotation_speed * delta_time;
|
||||||
}
|
}
|
||||||
if (ROTATE_LEFT) {
|
if (rotate_left) {
|
||||||
body_.angle -= config_.physics.rotation_speed * delta_time;
|
body_.angle -= config_.physics.rotation_speed * delta_time;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Thrust: fuerza vectorial en la dirección de la nariz.
|
// Thrust: fuerza vectorial en la dirección de la nariz.
|
||||||
// angle - PI/2 porque angle=0 apunta hacia arriba (eje Y negativo SDL).
|
// angle - PI/2 porque angle=0 apunta hacia arriba (eje Y negativo SDL).
|
||||||
if (THRUST) {
|
if (thrust) {
|
||||||
const float DIR_X = std::cos(body_.angle - (Constants::PI / 2.0F));
|
const float DIR_X = std::cos(body_.angle - (Constants::PI / 2.0F));
|
||||||
const float DIR_Y = std::sin(body_.angle - (Constants::PI / 2.0F));
|
const float DIR_Y = std::sin(body_.angle - (Constants::PI / 2.0F));
|
||||||
const float MAGNITUDE = body_.mass * config_.physics.acceleration;
|
const float MAGNITUDE = body_.mass * config_.physics.acceleration;
|
||||||
@@ -154,7 +162,7 @@ void Ship::draw() const {
|
|||||||
const float THRUST_MODULATOR = 1.0F + (VISUAL_PUSH / config_.visual_thrust.scale_divisor);
|
const float THRUST_MODULATOR = 1.0F + (VISUAL_PUSH / config_.visual_thrust.scale_divisor);
|
||||||
const float SCALE = config_.shape.scale * THRUST_MODULATOR;
|
const float SCALE = config_.shape.scale * THRUST_MODULATOR;
|
||||||
|
|
||||||
// Parpelleig daurat mentre està ferida: alterna color normal ↔ color hurt.
|
// Parpelleig mentre està ferida: alterna color normal ↔ color hurt.
|
||||||
SDL_Color color = config_.colors.normal;
|
SDL_Color color = config_.colors.normal;
|
||||||
if (hurt_timer_ > 0.0F) {
|
if (hurt_timer_ > 0.0F) {
|
||||||
const float CYCLE = 1.0F / config_.hurt.blink_hz;
|
const float CYCLE = 1.0F / config_.hurt.blink_hz;
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ class Ship : public Entities::Entity {
|
|||||||
void init() override { init(nullptr, false); }
|
void init() override { init(nullptr, false); }
|
||||||
void init(const Vec2* spawn_point, bool activar_invulnerabilitat = false);
|
void init(const Vec2* spawn_point, bool activar_invulnerabilitat = false);
|
||||||
void processInput(float delta_time, uint8_t player_id);
|
void processInput(float delta_time, uint8_t player_id);
|
||||||
|
// Aplica rotació/empenta des de booleans de control (mateixa física que
|
||||||
|
// processInput, però sense llegir Input). Usat pel pilot IA del mode demo.
|
||||||
|
void applyMovement(bool rotate_left, bool rotate_right, bool thrust, float delta_time);
|
||||||
void update(float delta_time) override;
|
void update(float delta_time) override;
|
||||||
void postUpdate(float delta_time) override;
|
void postUpdate(float delta_time) override;
|
||||||
void draw() const override;
|
void draw() const override;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include "game_scene.hpp"
|
#include "game_scene.hpp"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <ctime>
|
#include <ctime>
|
||||||
@@ -12,6 +13,7 @@
|
|||||||
#include "core/audio/audio.hpp"
|
#include "core/audio/audio.hpp"
|
||||||
#include "core/entities/entity_loader.hpp"
|
#include "core/entities/entity_loader.hpp"
|
||||||
#include "core/input/input.hpp"
|
#include "core/input/input.hpp"
|
||||||
|
#include "core/input/input_types.hpp"
|
||||||
#include "core/locale/locale.hpp"
|
#include "core/locale/locale.hpp"
|
||||||
#include "core/system/scene_context.hpp"
|
#include "core/system/scene_context.hpp"
|
||||||
#include "core/system/service_menu.hpp"
|
#include "core/system/service_menu.hpp"
|
||||||
@@ -21,6 +23,7 @@
|
|||||||
#include "game/stage_system/stage_loader.hpp"
|
#include "game/stage_system/stage_loader.hpp"
|
||||||
#include "game/systems/collision_system.hpp"
|
#include "game/systems/collision_system.hpp"
|
||||||
#include "game/systems/continue_system.hpp"
|
#include "game/systems/continue_system.hpp"
|
||||||
|
#include "game/systems/enemy_ai_system.hpp"
|
||||||
#include "game/systems/init_hud_animator.hpp"
|
#include "game/systems/init_hud_animator.hpp"
|
||||||
|
|
||||||
// Using declarations per simplificar el codi
|
// Using declarations per simplificar el codi
|
||||||
@@ -28,6 +31,25 @@ using SceneManager::SceneContext;
|
|||||||
using SceneType = SceneContext::SceneType;
|
using SceneType = SceneContext::SceneType;
|
||||||
using Option = SceneContext::Option;
|
using Option = SceneContext::Option;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// Attract mode: durada fixa de la demo. Amb vides infinites, sempre dura
|
||||||
|
// això (les morts respawnegen); només input o aquest timeout la tallen.
|
||||||
|
constexpr float DEMO_DURATION = 35.0F;
|
||||||
|
|
||||||
|
// Qualsevol d'aquestes accions trenca la demo i torna al títol.
|
||||||
|
constexpr std::array<InputAction, 5> DEMO_EXIT_ACTIONS = {
|
||||||
|
InputAction::LEFT,
|
||||||
|
InputAction::RIGHT,
|
||||||
|
InputAction::THRUST,
|
||||||
|
InputAction::SHOOT,
|
||||||
|
InputAction::START};
|
||||||
|
|
||||||
|
// Color de les frases d'inici/fi de fase (àmbar neon). És propi del joc i
|
||||||
|
// independent del "PULSA START" del títol (ara blanc): abans compartien la
|
||||||
|
// mateixa constant i en posar el títol en blanc aquestes frases també ho feien.
|
||||||
|
constexpr SDL_Color STAGE_MESSAGE_COLOR = {.r = 255, .g = 200, .b = 70, .a = 255};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
GameScene::GameScene(SDLManager& sdl, SceneContext& context)
|
GameScene::GameScene(SDLManager& sdl, SceneContext& context)
|
||||||
: sdl_(sdl),
|
: sdl_(sdl),
|
||||||
context_(context),
|
context_(context),
|
||||||
@@ -38,7 +60,8 @@ GameScene::GameScene(SDLManager& sdl, SceneContext& context)
|
|||||||
text_(sdl.getRenderer()),
|
text_(sdl.getRenderer()),
|
||||||
starfield_parallax_(sdl.getRenderer()),
|
starfield_parallax_(sdl.getRenderer()),
|
||||||
playfield_(sdl.getRenderer()),
|
playfield_(sdl.getRenderer()),
|
||||||
border_(sdl.getRenderer()) {
|
border_(sdl.getRenderer()),
|
||||||
|
curtain_(sdl.getRenderer()) {
|
||||||
// Recuperar configuración de match des del context
|
// Recuperar configuración de match des del context
|
||||||
match_config_ = context_.getMatchConfig();
|
match_config_ = context_.getMatchConfig();
|
||||||
|
|
||||||
@@ -82,7 +105,7 @@ GameScene::GameScene(SDLManager& sdl, SceneContext& context)
|
|||||||
|
|
||||||
// Inicialitzar naves: P1 amb el shape del YAML, P2 amb override visual.
|
// Inicialitzar naves: P1 amb el shape del YAML, P2 amb override visual.
|
||||||
ships_[0] = Ship(sdl.getRenderer(), *player_config); // Jugador 1: nau estàndard
|
ships_[0] = Ship(sdl.getRenderer(), *player_config); // Jugador 1: nau estàndard
|
||||||
ships_[1] = Ship(sdl.getRenderer(), *player_config, "ship2.shp"); // Jugador 2: interceptor amb ales
|
ships_[1] = Ship(sdl.getRenderer(), *player_config, "ship/wedge.shp"); // Jugador 2: triangle amb cercle central
|
||||||
|
|
||||||
// Inicialitzar balas con renderer
|
// Inicialitzar balas con renderer
|
||||||
std::ranges::fill(bullets_, Bullet(sdl.getRenderer()));
|
std::ranges::fill(bullets_, Bullet(sdl.getRenderer()));
|
||||||
@@ -138,10 +161,30 @@ GameScene::GameScene(SDLManager& sdl, SceneContext& context)
|
|||||||
|
|
||||||
// Initialize stage manager
|
// Initialize stage manager
|
||||||
stage_manager_ = std::make_unique<StageSystem::StageManager>(stage_config_.get());
|
stage_manager_ = std::make_unique<StageSystem::StageManager>(stage_config_.get());
|
||||||
|
if (match_config_.mode == GameConfig::Mode::DEMO) {
|
||||||
|
// Attract mode: arrencar directament en PLAYING a l'escenari curat
|
||||||
|
// actual (partida "ja començada") i avançar l'índex perquè la pròxima
|
||||||
|
// demo mostri un escenari diferent. El nombre de jugadors ja l'ha fixat
|
||||||
|
// TitleScene al match_config llegint el mateix escenari.
|
||||||
|
const Systems::Demo::Scenario SC = Systems::Demo::scenario(context_.demoScenarioIndex());
|
||||||
|
context_.advanceDemoScenario();
|
||||||
|
stage_manager_->initDemo(SC.stage);
|
||||||
|
demo_timer_ = DEMO_DURATION;
|
||||||
|
// Silenciar només els SFX de joc (Group::GAME) durant la demo: la música
|
||||||
|
// i els sons del menu de servei (Group::INTERFACE) segueixen sonant. No
|
||||||
|
// toquem el volum global ni la preferència de l'usuari.
|
||||||
|
Audio::get()->silenceGroup(Audio::Group::GAME, true);
|
||||||
|
// El fons (graella) ha d'aparèixer ja muntat: la demo és una partida en marxa.
|
||||||
|
playfield_.completeBuild();
|
||||||
|
// La cortinilla arrenca tapant i cau per destapar la demo (continua el
|
||||||
|
// moviment iniciat al títol, que va acabar amb la pantalla negra).
|
||||||
|
curtain_.reveal(Defaults::Game::Curtain::REVEAL_DURATION);
|
||||||
|
} else {
|
||||||
stage_manager_->init();
|
stage_manager_->init();
|
||||||
|
}
|
||||||
|
|
||||||
// Set ship position reference for safe spawn (P1 for now, TODO: dual tracking)
|
// Set ship position reference for safe spawn (P1 for now, TODO: dual tracking)
|
||||||
stage_manager_->getSpawnController().setShipPosition(&ships_[0].getCenter());
|
stage_manager_->getWaveRunner().setShipPosition(&ships_[0].getCenter());
|
||||||
|
|
||||||
// Inicialitzar timers de muerte per player
|
// Inicialitzar timers de muerte per player
|
||||||
hit_timer_per_player_[0] = 0.0F;
|
hit_timer_per_player_[0] = 0.0F;
|
||||||
@@ -185,7 +228,7 @@ GameScene::GameScene(SDLManager& sdl, SceneContext& context)
|
|||||||
// Registramos el body al world incluso inactivo: con radius=0 no colisiona
|
// Registramos el body al world incluso inactivo: con radius=0 no colisiona
|
||||||
// ni se mueve, y al init() del stage system se activa sin re-registrar.
|
// ni se mueve, y al init() del stage system se activa sin re-registrar.
|
||||||
for (auto& enemy : enemies_) {
|
for (auto& enemy : enemies_) {
|
||||||
enemy.setShipPosition(&ships_[0].getCenter()); // Set ship reference (P1 for now)
|
enemy.setShips(ships_.data(), &ships_[1]);
|
||||||
physics_world_.addBody(&enemy.getBody());
|
physics_world_.addBody(&enemy.getBody());
|
||||||
// DON'T call enemy.init() here - stage system handles spawning
|
// DON'T call enemy.init() here - stage system handles spawning
|
||||||
}
|
}
|
||||||
@@ -203,6 +246,18 @@ GameScene::GameScene(SDLManager& sdl, SceneContext& context)
|
|||||||
init_hud_rect_sound_played_ = false;
|
init_hud_rect_sound_played_ = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GameScene::~GameScene() {
|
||||||
|
// En sortir de la demo, primer parem qualsevol SFX encara sonant (p. ex. la
|
||||||
|
// veu de "fase completa" que la demo va llançar muteada): si no, en restaurar
|
||||||
|
// el volum del grup el motor reaplicaria la ganancia al canal viu i el so es
|
||||||
|
// colaria a la pantalla de títol. Després restaurem el grup GAME per al
|
||||||
|
// pròxim joc real.
|
||||||
|
if (match_config_.mode == GameConfig::Mode::DEMO) {
|
||||||
|
Audio::get()->stopAllSounds();
|
||||||
|
Audio::get()->silenceGroup(Audio::Group::GAME, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
auto GameScene::isFinished() const -> bool {
|
auto GameScene::isFinished() const -> bool {
|
||||||
return context_.nextScene() != SceneType::GAME;
|
return context_.nextScene() != SceneType::GAME;
|
||||||
}
|
}
|
||||||
@@ -214,10 +269,16 @@ void GameScene::handleEvent(const SDL_Event& event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void GameScene::update(float delta_time) {
|
void GameScene::update(float delta_time) {
|
||||||
// Pausa global: mentre el menu de servei esta obert, congelem la lògica
|
// El menu de servei "flota" sobre l'escena: l'input se'n va cap a ell, mai
|
||||||
// de joc. El draw() segueix executant-se per a mantenir l'escena visible
|
// "a baix". En partida normal pausem la lògica de joc mentre està obert (el
|
||||||
// sota el menu.
|
// draw() segueix executant-se per a mantindre l'escena visible sota el menu).
|
||||||
if (const auto* menu = System::ServiceMenu::get(); menu != nullptr && menu->isOpen()) {
|
// En DEMO (attract) NO pausem: la demo ha de seguir viva sota el menu; només
|
||||||
|
// evitem que l'input la trenque (flag MENU_OPEN cap a stepDemo). Amb l'overlay
|
||||||
|
// de redefinir controls actiu el menu segueix "open", així que aquest únic
|
||||||
|
// check ja cobreix tots dos casos.
|
||||||
|
const auto* menu = System::ServiceMenu::get();
|
||||||
|
const bool MENU_OPEN = (menu != nullptr) && menu->isOpen();
|
||||||
|
if (MENU_OPEN && match_config_.mode != GameConfig::Mode::DEMO) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,7 +286,12 @@ void GameScene::update(float delta_time) {
|
|||||||
// mantener update() legible y reducir complejidad cognitiva.
|
// mantener update() legible y reducir complejidad cognitiva.
|
||||||
stepPhysics(delta_time);
|
stepPhysics(delta_time);
|
||||||
|
|
||||||
if (game_over_state_ == GameOverState::NONE) {
|
if (match_config_.mode == GameConfig::Mode::DEMO) {
|
||||||
|
// Mode demo (attract): salida por input/timeout/muerte + control del pilot.
|
||||||
|
if (stepDemo(delta_time, MENU_OPEN)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (game_over_state_ == GameOverState::NONE) {
|
||||||
stepShootingInput();
|
stepShootingInput();
|
||||||
stepMidGameJoin();
|
stepMidGameJoin();
|
||||||
}
|
}
|
||||||
@@ -298,6 +364,75 @@ void GameScene::stepShootingInput() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GameScene::updateShipsControl(float delta_time) {
|
||||||
|
const bool DEMO = (match_config_.mode == GameConfig::Mode::DEMO);
|
||||||
|
for (uint8_t i = 0; i < 2; i++) {
|
||||||
|
const bool ACTIU = (i == 0) ? match_config_.player1_active : match_config_.player2_active;
|
||||||
|
if (!ACTIU || hit_timer_per_player_[i] != 0.0F) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// En demo, cada nau activa es mou amb el seu pilot IA (control calculat
|
||||||
|
// a stepDemo); la resta de casos llegeixen Input com sempre.
|
||||||
|
if (DEMO) {
|
||||||
|
ships_[i].applyMovement(demo_ctrls_[i].left, demo_ctrls_[i].right, demo_ctrls_[i].thrust, delta_time);
|
||||||
|
} else {
|
||||||
|
ships_[i].processInput(delta_time, i);
|
||||||
|
}
|
||||||
|
ships_[i].update(delta_time);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GameScene::stepDemo(float delta_time, bool input_blocked) -> bool {
|
||||||
|
curtain_.update(delta_time); // cortinilla que destapa la demo
|
||||||
|
|
||||||
|
// Qualsevol input trenca la demo i torna al títol (música intacta), pero NO
|
||||||
|
// mentre el menu de servei estiga obert: en eixe cas l'input va al menu i no
|
||||||
|
// arriba "a baix", així que la demo ha de seguir corrent sota seu.
|
||||||
|
if (!input_blocked && Input::get()->checkAnyPlayerAction(DEMO_EXIT_ACTIONS)) {
|
||||||
|
context_.setNextScene(SceneType::TITLE, Option::JUMP_TO_TITLE_MAIN);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vides infinites: la demo dura sempre el temps fix; en morir, stepDeathSequence
|
||||||
|
// respawneja (no acaba ni passa per CONTINUE/GAME_OVER).
|
||||||
|
demo_timer_ -= delta_time;
|
||||||
|
if (demo_timer_ <= 0.0F) {
|
||||||
|
endDemo();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Control de cada nau activa per al frame; el disparo el dispara GameScene.
|
||||||
|
for (uint8_t i = 0; i < 2; i++) {
|
||||||
|
const bool ACTIU = (i == 0) ? match_config_.player1_active : match_config_.player2_active;
|
||||||
|
if (!ACTIU || hit_timer_per_player_[i] != 0.0F) {
|
||||||
|
demo_ctrls_[i] = {}; // nau inactiva/morta: sense control
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Company per a evitar foc amic en demo de 2 naus: només es passa si l'altre
|
||||||
|
// jugador està actiu al match (un slot no usat tindria isActive()==true).
|
||||||
|
const uint8_t OTHER = 1U - i;
|
||||||
|
const bool OTHER_ACTIVE = (OTHER == 0) ? match_config_.player1_active
|
||||||
|
: match_config_.player2_active;
|
||||||
|
const Ship* teammate = OTHER_ACTIVE ? &ships_[OTHER] : nullptr;
|
||||||
|
demo_ctrls_[i] = demo_pilots_[i].compute(
|
||||||
|
ships_[i],
|
||||||
|
teammate,
|
||||||
|
enemies_,
|
||||||
|
bullets_,
|
||||||
|
Defaults::Zones::PLAYAREA,
|
||||||
|
delta_time);
|
||||||
|
if (demo_ctrls_[i].shoot) {
|
||||||
|
fireBullet(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameScene::endDemo() {
|
||||||
|
// No parem la música: title.ogg segueix sonant durant el cicle atrae.
|
||||||
|
context_.setNextScene(SceneType::LOGO);
|
||||||
|
}
|
||||||
|
|
||||||
void GameScene::stepMidGameJoin() {
|
void GameScene::stepMidGameJoin() {
|
||||||
// Permitir join solo durante PLAYING.
|
// Permitir join solo durante PLAYING.
|
||||||
if (stage_manager_->getState() != StageSystem::EstatStage::PLAYING) {
|
if (stage_manager_->getState() != StageSystem::EstatStage::PLAYING) {
|
||||||
@@ -353,6 +488,7 @@ auto GameScene::stepContinueScreen(float delta_time) -> bool {
|
|||||||
|
|
||||||
// Enemies, bullets y efectos siguen moviéndose en background.
|
// Enemies, bullets y efectos siguen moviéndose en background.
|
||||||
for (auto& enemy : enemies_) {
|
for (auto& enemy : enemies_) {
|
||||||
|
Systems::EnemyAi::move(enemy, delta_time);
|
||||||
enemy.update(delta_time);
|
enemy.update(delta_time);
|
||||||
}
|
}
|
||||||
for (auto& bullet : bullets_) {
|
for (auto& bullet : bullets_) {
|
||||||
@@ -373,12 +509,13 @@ auto GameScene::stepGameOver(float delta_time) -> bool {
|
|||||||
game_over_timer_ -= delta_time;
|
game_over_timer_ -= delta_time;
|
||||||
if (game_over_timer_ <= 0.0F) {
|
if (game_over_timer_ <= 0.0F) {
|
||||||
Audio::get()->stopMusic();
|
Audio::get()->stopMusic();
|
||||||
context_.setNextScene(SceneType::TITLE);
|
context_.setNextScene(SceneType::LOGO);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enemies, bullets y efectos siguen moviéndose como fondo.
|
// Enemies, bullets y efectos siguen moviéndose como fondo.
|
||||||
for (auto& enemy : enemies_) {
|
for (auto& enemy : enemies_) {
|
||||||
|
Systems::EnemyAi::move(enemy, delta_time);
|
||||||
enemy.update(delta_time);
|
enemy.update(delta_time);
|
||||||
}
|
}
|
||||||
for (auto& bullet : bullets_) {
|
for (auto& bullet : bullets_) {
|
||||||
@@ -405,6 +542,15 @@ void GameScene::stepDeathSequence(float delta_time) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// *** PHASE 3: RESPAWN OR GAME OVER ***
|
// *** PHASE 3: RESPAWN OR GAME OVER ***
|
||||||
|
// Mode demo: vides infinites. Respawn sempre, sense decrementar vides ni
|
||||||
|
// passar mai per CONTINUE/GAME_OVER — la demo dura el seu temps fix.
|
||||||
|
if (match_config_.mode == GameConfig::Mode::DEMO) {
|
||||||
|
Vec2 spawn_pos = getSpawnPoint(i);
|
||||||
|
ships_[i].init(&spawn_pos, /*activar_invulnerabilitat=*/true);
|
||||||
|
hit_timer_per_player_[i] = 0.0F;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
lives_per_player_[i]--;
|
lives_per_player_[i]--;
|
||||||
if (lives_per_player_[i] > 0) {
|
if (lives_per_player_[i] > 0) {
|
||||||
Vec2 spawn_pos = getSpawnPoint(i);
|
Vec2 spawn_pos = getSpawnPoint(i);
|
||||||
@@ -428,6 +574,7 @@ void GameScene::stepDeathSequence(float delta_time) {
|
|||||||
// aunque otros jugadores aún jueguen.
|
// aunque otros jugadores aún jueguen.
|
||||||
if (algun_mort) {
|
if (algun_mort) {
|
||||||
for (auto& enemy : enemies_) {
|
for (auto& enemy : enemies_) {
|
||||||
|
Systems::EnemyAi::move(enemy, delta_time);
|
||||||
enemy.update(delta_time);
|
enemy.update(delta_time);
|
||||||
}
|
}
|
||||||
for (auto& bullet : bullets_) {
|
for (auto& bullet : bullets_) {
|
||||||
@@ -492,13 +639,7 @@ void GameScene::runStageLevelStart(float delta_time) {
|
|||||||
stage_manager_->update(delta_time);
|
stage_manager_->update(delta_time);
|
||||||
|
|
||||||
// Ambas naves pueden moverse y disparar durante el intro.
|
// Ambas naves pueden moverse y disparar durante el intro.
|
||||||
for (uint8_t i = 0; i < 2; i++) {
|
updateShipsControl(delta_time);
|
||||||
const bool ACTIU = (i == 0) ? match_config_.player1_active : match_config_.player2_active;
|
|
||||||
if (ACTIU && hit_timer_per_player_[i] == 0.0F) {
|
|
||||||
ships_[i].processInput(delta_time, i);
|
|
||||||
ships_[i].update(delta_time);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (auto& bullet : bullets_) {
|
for (auto& bullet : bullets_) {
|
||||||
bullet.update(delta_time);
|
bullet.update(delta_time);
|
||||||
}
|
}
|
||||||
@@ -509,26 +650,22 @@ void GameScene::runStageLevelStart(float delta_time) {
|
|||||||
|
|
||||||
void GameScene::runStagePlaying(float delta_time) {
|
void GameScene::runStagePlaying(float delta_time) {
|
||||||
const bool PAUSE_SPAWN = (hit_timer_per_player_[0] > 0.0F && hit_timer_per_player_[1] > 0.0F);
|
const bool PAUSE_SPAWN = (hit_timer_per_player_[0] > 0.0F && hit_timer_per_player_[1] > 0.0F);
|
||||||
stage_manager_->getSpawnController().update(delta_time, enemies_, PAUSE_SPAWN);
|
stage_manager_->getWaveRunner().update(delta_time, enemies_, PAUSE_SPAWN);
|
||||||
|
|
||||||
// Stage completado: cuando al menos un jugador está vivo y todos los enemies muertos.
|
// Stage completado: cuando al menos un jugador está vivo y todas las onades emeses y arena buida.
|
||||||
const bool ALGU_VIU = (hit_timer_per_player_[0] == 0.0F || hit_timer_per_player_[1] == 0.0F);
|
const bool ALGU_VIU = (hit_timer_per_player_[0] == 0.0F || hit_timer_per_player_[1] == 0.0F);
|
||||||
if (ALGU_VIU && stage_manager_->getSpawnController().allEnemiesDestroyed(enemies_)) {
|
if (ALGU_VIU && stage_manager_->getWaveRunner().stageComplete(enemies_)) {
|
||||||
stage_manager_->markStageCompleted();
|
stage_manager_->markStageCompleted();
|
||||||
Audio::get()->playSound(Defaults::Sound::GOOD_JOB_COMMANDER, Audio::Group::GAME);
|
Audio::get()->playSound(Defaults::Sound::GOOD_JOB_COMMANDER, Audio::Group::GAME);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gameplay normal: ships activos + entidades + colisiones + efectos.
|
// Gameplay normal: ships activos + entidades + colisiones + efectos.
|
||||||
for (uint8_t i = 0; i < 2; i++) {
|
updateShipsControl(delta_time);
|
||||||
const bool ACTIU = (i == 0) ? match_config_.player1_active : match_config_.player2_active;
|
auto ai_ctx = buildCollisionContext();
|
||||||
if (ACTIU && hit_timer_per_player_[i] == 0.0F) {
|
for (std::size_t i = 0; i < enemies_.size(); ++i) {
|
||||||
ships_[i].processInput(delta_time, i);
|
Systems::EnemyAi::tick(ai_ctx, enemies_[i], i, delta_time);
|
||||||
ships_[i].update(delta_time);
|
enemies_[i].update(delta_time);
|
||||||
}
|
|
||||||
}
|
|
||||||
for (auto& enemy : enemies_) {
|
|
||||||
enemy.update(delta_time);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Col·lisions primer, després desactivació per fora-de-zone: així una bala que
|
// Col·lisions primer, després desactivació per fora-de-zone: així una bala que
|
||||||
@@ -546,13 +683,7 @@ void GameScene::runStagePlaying(float delta_time) {
|
|||||||
|
|
||||||
void GameScene::runStageLevelCompleted(float delta_time) {
|
void GameScene::runStageLevelCompleted(float delta_time) {
|
||||||
stage_manager_->update(delta_time);
|
stage_manager_->update(delta_time);
|
||||||
for (uint8_t i = 0; i < 2; i++) {
|
updateShipsControl(delta_time);
|
||||||
const bool ACTIU = (i == 0) ? match_config_.player1_active : match_config_.player2_active;
|
|
||||||
if (ACTIU && hit_timer_per_player_[i] == 0.0F) {
|
|
||||||
ships_[i].processInput(delta_time, i);
|
|
||||||
ships_[i].update(delta_time);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (auto& bullet : bullets_) {
|
for (auto& bullet : bullets_) {
|
||||||
bullet.update(delta_time);
|
bullet.update(delta_time);
|
||||||
}
|
}
|
||||||
@@ -562,8 +693,8 @@ void GameScene::runStageLevelCompleted(float delta_time) {
|
|||||||
floating_score_manager_.update(delta_time);
|
floating_score_manager_.update(delta_time);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameScene::runCollisionDetections() {
|
auto GameScene::buildCollisionContext() -> Systems::Collision::Context {
|
||||||
Systems::Collision::Context col_ctx{
|
return Systems::Collision::Context{
|
||||||
.ships = ships_,
|
.ships = ships_,
|
||||||
.enemies = enemies_,
|
.enemies = enemies_,
|
||||||
.bullets = bullets_,
|
.bullets = bullets_,
|
||||||
@@ -574,8 +705,12 @@ void GameScene::runCollisionDetections() {
|
|||||||
.firework_manager = firework_manager_,
|
.firework_manager = firework_manager_,
|
||||||
.floating_score_manager = floating_score_manager_,
|
.floating_score_manager = floating_score_manager_,
|
||||||
.match_config = match_config_,
|
.match_config = match_config_,
|
||||||
.on_player_hit = [this](uint8_t pid) { tocado(pid); },
|
.on_player_hit = [this](uint8_t pid, const Vec2& bv) { tocado(pid, bv); },
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameScene::runCollisionDetections() {
|
||||||
|
auto col_ctx = buildCollisionContext();
|
||||||
Systems::Collision::detectAll(col_ctx);
|
Systems::Collision::detectAll(col_ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -604,6 +739,10 @@ void GameScene::draw() {
|
|||||||
drawLevelCompletedState();
|
drawLevelCompletedState();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cortinilla d'entrada de la demo: per damunt de tot. No-op fora del mode
|
||||||
|
// DEMO (curtain_ mai s'arrenca) i quan ja ha sortit per baix.
|
||||||
|
curtain_.draw();
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameScene::drawEnemies() const {
|
void GameScene::drawEnemies() const {
|
||||||
@@ -700,7 +839,7 @@ void GameScene::drawInitHudState() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (score_progress > 0.0F) {
|
if (score_progress > 0.0F) {
|
||||||
Systems::InitHud::drawScoreboardAnimated(text_, buildScoreboard(), score_progress);
|
Systems::InitHud::drawScoreboardAnimated(sdl_.getRenderer(), text_, buildScoreboardData(), score_progress);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ship1_progress > 0.0F && match_config_.player1_active && ships_[0].isActive()) {
|
if (ship1_progress > 0.0F && match_config_.player1_active && ships_[0].isActive()) {
|
||||||
@@ -754,7 +893,7 @@ void GameScene::drawLevelCompletedState() {
|
|||||||
drawScoreboard();
|
drawScoreboard();
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameScene::tocado(uint8_t player_id) {
|
void GameScene::tocado(uint8_t player_id, const Vec2& bullet_velocity) {
|
||||||
// Death sequence: 3 phases
|
// Death sequence: 3 phases
|
||||||
// Phase 1: First call (hit_timer_per_player_[player_id] == 0) - trigger explosion
|
// Phase 1: First call (hit_timer_per_player_[player_id] == 0) - trigger explosion
|
||||||
// Phase 2: Animation (0 < itocado_ < 3.0s) - debris animation
|
// Phase 2: Animation (0 < itocado_ < 3.0s) - debris animation
|
||||||
@@ -778,6 +917,9 @@ void GameScene::tocado(uint8_t player_id) {
|
|||||||
|
|
||||||
// Mateixa dispersió i efecte que els debris d'enemic (lifetime,
|
// Mateixa dispersió i efecte que els debris d'enemic (lifetime,
|
||||||
// friction, segment_multiplier alineats); només canvien sound i color.
|
// friction, segment_multiplier alineats); només canvien sound i color.
|
||||||
|
// bullet_velocity arriba a explode() com a impuls extra independent
|
||||||
|
// de la inèrcia del cos del ship — els trossos volen amb la força
|
||||||
|
// de la bala encara que el ship estiga quiet.
|
||||||
debris_manager_.explode(
|
debris_manager_.explode(
|
||||||
ships_[player_id].getShape(),
|
ships_[player_id].getShape(),
|
||||||
SHIP_POS,
|
SHIP_POS,
|
||||||
@@ -788,11 +930,12 @@ void GameScene::tocado(uint8_t player_id) {
|
|||||||
INHERITED_VEL,
|
INHERITED_VEL,
|
||||||
0.0F, // sense herència angular
|
0.0F, // sense herència angular
|
||||||
0.0F, // sin herencia visual
|
0.0F, // sin herencia visual
|
||||||
Defaults::Sound::EXPLOSION2,
|
Defaults::Sound::PLAYER_EXPLOSION,
|
||||||
ships_[player_id].getConfig().colors.normal,
|
ships_[player_id].getConfig().colors.normal,
|
||||||
Defaults::Physics::Debris::ENEMY_LIFETIME,
|
Defaults::Physics::Debris::ENEMY_LIFETIME,
|
||||||
Defaults::Physics::Debris::ENEMY_FRICTION,
|
Defaults::Physics::Debris::ENEMY_FRICTION,
|
||||||
Defaults::Physics::Debris::ENEMY_SEGMENT_MULTIPLIER);
|
Defaults::Physics::Debris::ENEMY_SEGMENT_MULTIPLIER,
|
||||||
|
bullet_velocity);
|
||||||
|
|
||||||
// Start death timer (non-zero to avoid re-triggering)
|
// Start death timer (non-zero to avoid re-triggering)
|
||||||
hit_timer_per_player_[player_id] = Defaults::Game::HIT_TIMER_TRIGGER_DEATH;
|
hit_timer_per_player_[player_id] = Defaults::Game::HIT_TIMER_TRIGGER_DEATH;
|
||||||
@@ -802,59 +945,50 @@ void GameScene::tocado(uint8_t player_id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void GameScene::drawScoreboard() {
|
void GameScene::drawScoreboard() {
|
||||||
// Construir text del marcador
|
|
||||||
std::string text = buildScoreboard();
|
|
||||||
|
|
||||||
// Parámetros de renderització
|
|
||||||
const float SCALE = Defaults::Hud::SCOREBOARD_TEXT_SCALE;
|
const float SCALE = Defaults::Hud::SCOREBOARD_TEXT_SCALE;
|
||||||
const float SPACING = Defaults::Hud::SCOREBOARD_TEXT_SPACING;
|
const float SPACING = Defaults::Hud::SCOREBOARD_TEXT_SPACING;
|
||||||
|
|
||||||
// Calcular centro de la zone del marcador
|
|
||||||
const SDL_FRect& scoreboard_zone = Defaults::Zones::SCOREBOARD;
|
const SDL_FRect& scoreboard_zone = Defaults::Zones::SCOREBOARD;
|
||||||
float center_x = scoreboard_zone.w / 2.0F;
|
const Vec2 CENTER = {
|
||||||
float center_y = scoreboard_zone.y + (scoreboard_zone.h / 2.0F);
|
.x = scoreboard_zone.w / 2.0F,
|
||||||
|
.y = scoreboard_zone.y + (scoreboard_zone.h / 2.0F),
|
||||||
// Renderizar centrat
|
};
|
||||||
text_.renderCentered(text, {.x = center_x, .y = center_y}, SCALE, SPACING);
|
// En mode demo (attract) el marcador no té sentit: substituïm puntuacions i
|
||||||
|
// vides per un rètol que indica que és una demo i convida a jugar.
|
||||||
|
if (match_config_.mode == GameConfig::Mode::DEMO) {
|
||||||
|
text_.renderCentered(Locale::get().text("demo.banner"), CENTER, SCALE, SPACING);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Systems::InitHud::drawScoreboardAt(sdl_.getRenderer(), text_, buildScoreboardData(), CENTER.y, SCALE, SPACING);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto GameScene::buildScoreboard() const -> std::string {
|
auto GameScene::buildScoreboardData() const -> Systems::InitHud::ScoreboardData {
|
||||||
// Puntuación P1 (6 dígits) - mostrar zeros si inactiu
|
Systems::InitHud::ScoreboardData out;
|
||||||
std::string score_p1;
|
|
||||||
std::string vides_p1;
|
|
||||||
if (match_config_.player1_active) {
|
|
||||||
score_p1 = std::to_string(score_per_player_[0]);
|
|
||||||
score_p1 = std::string(6 - std::min(6, static_cast<int>(score_p1.length())), '0') + score_p1;
|
|
||||||
vides_p1 = (lives_per_player_[0] < 10)
|
|
||||||
? "0" + std::to_string(lives_per_player_[0])
|
|
||||||
: std::to_string(lives_per_player_[0]);
|
|
||||||
} else {
|
|
||||||
score_p1 = "000000";
|
|
||||||
vides_p1 = "00";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nivel (2 dígits)
|
// Puntuació a 6 dígits amb zeros a l'esquerra (inactiu → tot zeros, 0 vides).
|
||||||
uint8_t stage_num = stage_manager_->getCurrentStage();
|
const auto FORMAT_SCORE = [](int score) {
|
||||||
std::string stage_str = (stage_num < 10) ? "0" + std::to_string(stage_num)
|
const std::string S = std::to_string(score);
|
||||||
: std::to_string(stage_num);
|
return std::string(6 - std::min(6, static_cast<int>(S.length())), '0') + S;
|
||||||
|
};
|
||||||
|
|
||||||
// Puntuación P2 (6 dígits) - mostrar zeros si inactiu
|
out.p1_active = match_config_.player1_active;
|
||||||
std::string score_p2;
|
out.p2_active = match_config_.player2_active;
|
||||||
std::string vides_p2;
|
out.score_p1 = match_config_.player1_active ? FORMAT_SCORE(score_per_player_[0]) : "000000";
|
||||||
if (match_config_.player2_active) {
|
out.lives_p1 = match_config_.player1_active ? lives_per_player_[0] : 0;
|
||||||
score_p2 = std::to_string(score_per_player_[1]);
|
out.score_p2 = match_config_.player2_active ? FORMAT_SCORE(score_per_player_[1]) : "000000";
|
||||||
score_p2 = std::string(6 - std::min(6, static_cast<int>(score_p2.length())), '0') + score_p2;
|
out.lives_p2 = match_config_.player2_active ? lives_per_player_[1] : 0;
|
||||||
vides_p2 = (lives_per_player_[1] < 10)
|
|
||||||
? "0" + std::to_string(lives_per_player_[1])
|
|
||||||
: std::to_string(lives_per_player_[1]);
|
|
||||||
} else {
|
|
||||||
score_p2 = "000000";
|
|
||||||
vides_p2 = "00";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format: "123456 03 LEVEL 01 654321 02"
|
// Shapes de les naus per a les icones de vides (reutilitza la geometria ja
|
||||||
// Nota: dos espais entre seccions, mantenir ambdós slots siempre visibles
|
// carregada de cada Ship).
|
||||||
return score_p1 + " " + vides_p1 + " " + Locale::get().text("hud.level") + stage_str + " " + score_p2 + " " + vides_p2;
|
out.shape_p1 = ships_[0].getShape();
|
||||||
|
out.shape_p2 = ships_[1].getShape();
|
||||||
|
|
||||||
|
// Nivell: etiqueta localitzada + número a 2 dígits (separats per pintar-los
|
||||||
|
// amb tonalitats distintes).
|
||||||
|
const uint8_t STAGE_NUM = stage_manager_->getCurrentStage();
|
||||||
|
out.level_label = Locale::get().text("hud.level");
|
||||||
|
out.level_value = (STAGE_NUM < 10) ? "0" + std::to_string(STAGE_NUM)
|
||||||
|
: std::to_string(STAGE_NUM);
|
||||||
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
// [NEW] Stage system helper methods
|
// [NEW] Stage system helper methods
|
||||||
@@ -922,7 +1056,7 @@ void GameScene::drawStageMessage(const std::string& message) {
|
|||||||
// Render only the partial message (typewriter effect) amb el color
|
// Render only the partial message (typewriter effect) amb el color
|
||||||
// ambre neon del "PRESS START" del títol — unifica el feel dels missatges.
|
// ambre neon del "PRESS START" del títol — unifica el feel dels missatges.
|
||||||
Vec2 pos = {.x = x, .y = y};
|
Vec2 pos = {.x = x, .y = y};
|
||||||
text_.render(partial_message, pos, scale, SPACING, 1.0F, Defaults::Title::Colors::PRESS_START);
|
text_.render(partial_message, pos, scale, SPACING, 1.0F, STAGE_MESSAGE_COLOR);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include "core/graphics/border.hpp"
|
#include "core/graphics/border.hpp"
|
||||||
|
#include "core/graphics/curtain.hpp"
|
||||||
#include "core/graphics/playfield.hpp"
|
#include "core/graphics/playfield.hpp"
|
||||||
#include "core/graphics/starfield_parallax.hpp"
|
#include "core/graphics/starfield_parallax.hpp"
|
||||||
#include "core/graphics/vector_text.hpp"
|
#include "core/graphics/vector_text.hpp"
|
||||||
@@ -28,6 +29,9 @@
|
|||||||
#include "game/entities/ship.hpp"
|
#include "game/entities/ship.hpp"
|
||||||
#include "game/stage_system/stage_config.hpp"
|
#include "game/stage_system/stage_config.hpp"
|
||||||
#include "game/stage_system/stage_manager.hpp"
|
#include "game/stage_system/stage_manager.hpp"
|
||||||
|
#include "game/systems/collision_system.hpp"
|
||||||
|
#include "game/systems/demo_pilot.hpp"
|
||||||
|
#include "game/systems/init_hud_animator.hpp"
|
||||||
|
|
||||||
// Game over state machine
|
// Game over state machine
|
||||||
enum class GameOverState : uint8_t {
|
enum class GameOverState : uint8_t {
|
||||||
@@ -40,7 +44,7 @@ enum class GameOverState : uint8_t {
|
|||||||
class GameScene final : public Scene {
|
class GameScene final : public Scene {
|
||||||
public:
|
public:
|
||||||
explicit GameScene(SDLManager& sdl, SceneManager::SceneContext& context);
|
explicit GameScene(SDLManager& sdl, SceneManager::SceneContext& context);
|
||||||
~GameScene() override = default;
|
~GameScene() override; // Restaura l'estat dels SFX si la demo els ha silenciat
|
||||||
|
|
||||||
// Scene interface
|
// Scene interface
|
||||||
void handleEvent(const SDL_Event& event) override;
|
void handleEvent(const SDL_Event& event) override;
|
||||||
@@ -67,7 +71,7 @@ class GameScene final : public Scene {
|
|||||||
std::array<Enemy, Constants::MAX_ORNIS> enemies_;
|
std::array<Enemy, Constants::MAX_ORNIS> enemies_;
|
||||||
// 6 balas: P1=[0,1,2], P2=[3,4,5]. El cast a size_t evita la
|
// 6 balas: P1=[0,1,2], P2=[3,4,5]. El cast a size_t evita la
|
||||||
// widening conversion implícita que detecta clang-tidy.
|
// widening conversion implícita que detecta clang-tidy.
|
||||||
std::array<Bullet, static_cast<std::size_t>(Constants::MAX_BULLETS) * 2> bullets_;
|
std::array<Bullet, static_cast<std::size_t>(Defaults::Entities::MAX_BULLETS_TOTAL)> bullets_;
|
||||||
std::array<float, 2> hit_timer_per_player_; // Death timers per player (seconds)
|
std::array<float, 2> hit_timer_per_player_; // Death timers per player (seconds)
|
||||||
|
|
||||||
// Lives and game over system
|
// Lives and game over system
|
||||||
@@ -92,6 +96,9 @@ class GameScene final : public Scene {
|
|||||||
// Border del playfield (4 línies amb desplaçaments i flash per impactes)
|
// Border del playfield (4 línies amb desplaçaments i flash per impactes)
|
||||||
Graphics::Border border_;
|
Graphics::Border border_;
|
||||||
|
|
||||||
|
// Cortinilla que destapa en entrar a la demo (només mode DEMO; inactiva en partida normal).
|
||||||
|
Graphics::Curtain curtain_;
|
||||||
|
|
||||||
// [NEW] Stage system
|
// [NEW] Stage system
|
||||||
std::unique_ptr<StageSystem::StageSystemConfig> stage_config_;
|
std::unique_ptr<StageSystem::StageSystemConfig> stage_config_;
|
||||||
std::unique_ptr<StageSystem::StageManager> stage_manager_;
|
std::unique_ptr<StageSystem::StageManager> stage_manager_;
|
||||||
@@ -99,8 +106,16 @@ class GameScene final : public Scene {
|
|||||||
// Control de sons de animación INIT_HUD
|
// Control de sons de animación INIT_HUD
|
||||||
bool init_hud_rect_sound_played_{false}; // Flag para evitar repetir sonido del rectángulo
|
bool init_hud_rect_sound_played_{false}; // Flag para evitar repetir sonido del rectángulo
|
||||||
|
|
||||||
|
// Attract mode (mode DEMO): un pilot IA per nau activa (1 o 2 jugadors).
|
||||||
|
std::array<Systems::Demo::DemoPilot, 2> demo_pilots_;
|
||||||
|
std::array<Systems::Demo::Control, 2> demo_ctrls_{}; // Control per nau al frame actual
|
||||||
|
float demo_timer_{0.0F}; // Temps restant de la demo (→ LOGO)
|
||||||
|
|
||||||
// Funciones privades
|
// Funciones privades
|
||||||
void tocado(uint8_t player_id);
|
// bullet_velocity: velocitat de la bala que ha causat la mort (Vec2{} si no
|
||||||
|
// ve d'una bala). Es passa al debris perquè els fragments volin en direcció
|
||||||
|
// de la bala (independent de la inèrcia del cos del ship).
|
||||||
|
void tocado(uint8_t player_id, const Vec2& bullet_velocity = {.x = 0.0F, .y = 0.0F});
|
||||||
void drawScoreboard(); // Dibuixar marcador de puntuación
|
void drawScoreboard(); // Dibuixar marcador de puntuación
|
||||||
void fireBullet(uint8_t player_id); // Shoot bullet from player
|
void fireBullet(uint8_t player_id); // Shoot bullet from player
|
||||||
[[nodiscard]] auto getSpawnPoint(uint8_t player_id) const -> Vec2; // Get spawn position for player
|
[[nodiscard]] auto getSpawnPoint(uint8_t player_id) const -> Vec2; // Get spawn position for player
|
||||||
@@ -124,14 +139,24 @@ class GameScene final : public Scene {
|
|||||||
void drawPlayingState();
|
void drawPlayingState();
|
||||||
void drawLevelCompletedState();
|
void drawLevelCompletedState();
|
||||||
|
|
||||||
// [NEW] Función helper del marcador
|
// Helper del marcador: construeix les dades (puntuacions, vides i nivell)
|
||||||
[[nodiscard]] auto buildScoreboard() const -> std::string;
|
// per al render en blocs ancorats per jugador.
|
||||||
|
[[nodiscard]] auto buildScoreboardData() const -> Systems::InitHud::ScoreboardData;
|
||||||
|
|
||||||
// Sub-pasos de update() (descompuestos en Fase 9d para reducir
|
// Sub-pasos de update() (descompuestos en Fase 9d para reducir
|
||||||
// complejidad cognitiva; cada uno es responsable de una sección).
|
// complejidad cognitiva; cada uno es responsable de una sección).
|
||||||
void stepPhysics(float delta_time);
|
void stepPhysics(float delta_time);
|
||||||
void stepShootingInput();
|
void stepShootingInput();
|
||||||
void stepMidGameJoin();
|
void stepMidGameJoin();
|
||||||
|
// Mueve las naves activas: en mode DEMO cada nau usa su pilot IA (demo_ctrls_),
|
||||||
|
// el resto usa processInput (Input). Compartido por los 3 estados jugables.
|
||||||
|
void updateShipsControl(float delta_time);
|
||||||
|
// Mode DEMO: gestiona salida (input→título, timeout/muerte→logo) y calcula
|
||||||
|
// el control + disparo del pilot. Devuelve true si la escena transiciona.
|
||||||
|
// input_blocked: si un overlay (menu de servei / redefinir controls) està
|
||||||
|
// capturant l'input, no es deixa que l'input trenque la demo (segueix viva).
|
||||||
|
[[nodiscard]] auto stepDemo(float delta_time, bool input_blocked) -> bool;
|
||||||
|
void endDemo();
|
||||||
// Devuelven true si el frame debe salir tras esta sección.
|
// Devuelven true si el frame debe salir tras esta sección.
|
||||||
[[nodiscard]] auto stepContinueScreen(float delta_time) -> bool;
|
[[nodiscard]] auto stepContinueScreen(float delta_time) -> bool;
|
||||||
[[nodiscard]] auto stepGameOver(float delta_time) -> bool;
|
[[nodiscard]] auto stepGameOver(float delta_time) -> bool;
|
||||||
@@ -144,6 +169,10 @@ class GameScene final : public Scene {
|
|||||||
void runStageLevelStart(float delta_time);
|
void runStageLevelStart(float delta_time);
|
||||||
void runStagePlaying(float delta_time);
|
void runStagePlaying(float delta_time);
|
||||||
void runStageLevelCompleted(float delta_time);
|
void runStageLevelCompleted(float delta_time);
|
||||||
|
// Construeix el Collision::Context del frame actual (referències a tots els
|
||||||
|
// pools/managers + on_player_hit). Reutilitzat tant per al tick d'IA com
|
||||||
|
// per a runCollisionDetections.
|
||||||
|
[[nodiscard]] auto buildCollisionContext() -> Systems::Collision::Context;
|
||||||
// Helper: ejecuta colisiones de gameplay con el Context preparado.
|
// Helper: ejecuta colisiones de gameplay con el Context preparado.
|
||||||
void runCollisionDetections();
|
void runCollisionDetections();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -55,6 +55,11 @@ LogoScene::LogoScene(SDLManager& sdl, SceneContext& context)
|
|||||||
(void)option; // Suprimir warning
|
(void)option; // Suprimir warning
|
||||||
|
|
||||||
sound_played_.fill(false); // Inicialitzar seguiment de sons
|
sound_played_.fill(false); // Inicialitzar seguiment de sons
|
||||||
|
|
||||||
|
// Si ja sona música (venim de la demo del cicle d'atracció), operar en
|
||||||
|
// silenci: ni sons de lletres ni reinici de title.ogg.
|
||||||
|
attract_silent_ = (Audio::getMusicState() == Audio::MusicState::PLAYING);
|
||||||
|
|
||||||
initLetters();
|
initLetters();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,8 +176,11 @@ void LogoScene::changeState(AnimationState nou_estat) {
|
|||||||
std::mt19937 g(rd());
|
std::mt19937 g(rd());
|
||||||
std::shuffle(explosion_order_.begin(), explosion_order_.end(), g);
|
std::shuffle(explosion_order_.begin(), explosion_order_.end(), g);
|
||||||
} else if (nou_estat == AnimationState::POST_EXPLOSION) {
|
} else if (nou_estat == AnimationState::POST_EXPLOSION) {
|
||||||
|
// En el cicle d'atracció la música ja sona; no la reiniciem.
|
||||||
|
if (!attract_silent_) {
|
||||||
Audio::get()->playMusic("title.ogg");
|
Audio::get()->playMusic("title.ogg");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
std::cout << "[LogoScene] Canvi a state: " << static_cast<int>(nou_estat)
|
std::cout << "[LogoScene] Canvi a state: " << static_cast<int>(nou_estat)
|
||||||
<< "\n";
|
<< "\n";
|
||||||
@@ -200,8 +208,12 @@ void LogoScene::updateExplosions(float delta_time) {
|
|||||||
FINAL_SCALE, // Escala (lletres a scale final)
|
FINAL_SCALE, // Escala (lletres a scale final)
|
||||||
SPEED_EXPLOSIO, // Velocidad base
|
SPEED_EXPLOSIO, // Velocidad base
|
||||||
1.0F, // Brightness màxim (per defecte)
|
1.0F, // Brightness màxim (per defecte)
|
||||||
{.x = 0.0F, .y = 0.0F} // Sin velocity (per defecte)
|
{.x = 0.0F, .y = 0.0F}, // Sin velocity (per defecte)
|
||||||
);
|
0.0F, // Velocitat angular (per defecte)
|
||||||
|
0.0F, // Factor herència visual (per defecte)
|
||||||
|
// Dins el cicle d'atracció les explosions són mudes; fora, so
|
||||||
|
// d'explosió per defecte.
|
||||||
|
attract_silent_ ? "" : Defaults::Sound::ENEMY_EXPLOSION);
|
||||||
|
|
||||||
std::cout << "[LogoScene] Explota letter " << letter_explosion_index_ << "\n";
|
std::cout << "[LogoScene] Explota letter " << letter_explosion_index_ << "\n";
|
||||||
|
|
||||||
@@ -237,9 +249,13 @@ void LogoScene::update(float delta_time) {
|
|||||||
global_progress,
|
global_progress,
|
||||||
LETTER_THRESHOLD);
|
LETTER_THRESHOLD);
|
||||||
|
|
||||||
// Reproduir so cuando la letter comença a aparèixer (progress > 0)
|
// Reproduir so cuando la letter comença a aparèixer (progress > 0).
|
||||||
|
// En mode silenciós (cicle d'atracció) saltem el so però igualment
|
||||||
|
// marquem la letter per no acumular pendents.
|
||||||
if (letter_progress > 0.0F) {
|
if (letter_progress > 0.0F) {
|
||||||
|
if (!attract_silent_) {
|
||||||
Audio::get()->playSound(Defaults::Sound::LOGO, Audio::Group::GAME);
|
Audio::get()->playSound(Defaults::Sound::LOGO, Audio::Group::GAME);
|
||||||
|
}
|
||||||
sound_played_[i] = true;
|
sound_played_[i] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,6 +67,11 @@ class LogoScene final : public Scene {
|
|||||||
// Seguiment de sons de lletres (evitar reproduccions repetides)
|
// Seguiment de sons de lletres (evitar reproduccions repetides)
|
||||||
std::array<bool, 9> sound_played_; // Track si cada letter ya ha reproduit el so
|
std::array<bool, 9> sound_played_; // Track si cada letter ya ha reproduit el so
|
||||||
|
|
||||||
|
// Cicle d'atracció: si en entrar al logo ja hi ha música sonant (venim de la
|
||||||
|
// demo amb title.ogg en marxa), el logo no ha d'emetre sons ni reiniciar la
|
||||||
|
// música — només repintar-se en silenci.
|
||||||
|
bool attract_silent_{false};
|
||||||
|
|
||||||
// Constants de animación
|
// Constants de animación
|
||||||
static constexpr float DURATION_PRE = 1.5F; // Duració PRE_ANIMATION (pantalla negra)
|
static constexpr float DURATION_PRE = 1.5F; // Duració PRE_ANIMATION (pantalla negra)
|
||||||
static constexpr float DURATION_ZOOM = 4.0F; // Duració del zoom (segons)
|
static constexpr float DURATION_ZOOM = 4.0F; // Duració del zoom (segons)
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
#include <cfloat>
|
#include <cfloat>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <numbers>
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include "core/audio/audio.hpp"
|
#include "core/audio/audio.hpp"
|
||||||
@@ -20,6 +19,7 @@
|
|||||||
#include "core/rendering/shape_renderer.hpp"
|
#include "core/rendering/shape_renderer.hpp"
|
||||||
#include "core/system/scene_context.hpp"
|
#include "core/system/scene_context.hpp"
|
||||||
#include "core/system/service_menu.hpp"
|
#include "core/system/service_menu.hpp"
|
||||||
|
#include "game/systems/demo_pilot.hpp"
|
||||||
#include "project.h"
|
#include "project.h"
|
||||||
|
|
||||||
using SceneManager::SceneContext;
|
using SceneManager::SceneContext;
|
||||||
@@ -36,7 +36,8 @@ namespace {
|
|||||||
TitleScene::TitleScene(SDLManager& sdl, SceneContext& context)
|
TitleScene::TitleScene(SDLManager& sdl, SceneContext& context)
|
||||||
: sdl_(sdl),
|
: sdl_(sdl),
|
||||||
context_(context),
|
context_(context),
|
||||||
text_(sdl.getRenderer()) {
|
text_(sdl.getRenderer()),
|
||||||
|
curtain_(sdl.getRenderer()) {
|
||||||
std::cout << "SceneType Titol: Inicialitzant...\n";
|
std::cout << "SceneType Titol: Inicialitzant...\n";
|
||||||
|
|
||||||
match_config_.player1_active = false;
|
match_config_.player1_active = false;
|
||||||
@@ -78,7 +79,7 @@ TitleScene::TitleScene(SDLManager& sdl, SceneContext& context)
|
|||||||
|
|
||||||
// Flash que tapa el "pop" final de la nau al VP. Es spawneja al centre
|
// Flash que tapa el "pop" final de la nau al VP. Es spawneja al centre
|
||||||
// de pantalla (= projecció del VP) quan ship_animator avisa.
|
// de pantalla (= projecció del VP) quan ship_animator avisa.
|
||||||
flash_shape_ = Graphics::ShapeLoader::load("title_flash.shp");
|
flash_shape_ = Graphics::ShapeLoader::load("effect/title_flash.shp");
|
||||||
ship_animator_->setOnShipDisappear([this](int /*player_id*/) {
|
ship_animator_->setOnShipDisappear([this](int /*player_id*/) {
|
||||||
triggerFlash(Vec2{
|
triggerFlash(Vec2{
|
||||||
.x = static_cast<float>(Defaults::Window::WIDTH) / 2.0F,
|
.x = static_cast<float>(Defaults::Window::WIDTH) / 2.0F,
|
||||||
@@ -94,8 +95,16 @@ TitleScene::TitleScene(SDLManager& sdl, SceneContext& context)
|
|||||||
}
|
}
|
||||||
|
|
||||||
TitleScene::~TitleScene() {
|
TitleScene::~TitleScene() {
|
||||||
|
// Attract mode: si saltem a la demo, NO parem la música — ha de seguir
|
||||||
|
// sonant durant tot el cicle TÍTOL→DEMO→LOGO→TÍTOL. La resta de sortides
|
||||||
|
// (partida normal, EXIT) sí paren.
|
||||||
|
const bool ENTERING_DEMO =
|
||||||
|
context_.nextScene() == SceneType::GAME &&
|
||||||
|
context_.getMatchConfig().mode == GameConfig::Mode::DEMO;
|
||||||
|
if (!ENTERING_DEMO) {
|
||||||
Audio::get()->stopMusic();
|
Audio::get()->stopMusic();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void TitleScene::initTitle() {
|
void TitleScene::initTitle() {
|
||||||
using namespace Graphics;
|
using namespace Graphics;
|
||||||
@@ -267,8 +276,9 @@ void TitleScene::dibuixarPeuTitol(float spacing) const {
|
|||||||
// a 1.0 (a la mida i posició finals, "lluny" al VP).
|
// a 1.0 (a la mida i posició finals, "lluny" al VP).
|
||||||
const float SCREEN_CENTRE_X = Defaults::Game::WIDTH / 2.0F;
|
const float SCREEN_CENTRE_X = Defaults::Game::WIDTH / 2.0F;
|
||||||
const float SCREEN_CENTRE_Y = Defaults::Game::HEIGHT / 2.0F;
|
const float SCREEN_CENTRE_Y = Defaults::Game::HEIGHT / 2.0F;
|
||||||
const float JAILGAMES_S = std::lerp(S::FOOTER_INTRO_SCALE_START, 1.0F, Easing::easeOutQuad(intro_jailgames_progress_));
|
// dive_zoom_ (attract) afegeix el zoom del dive per travessar el peu.
|
||||||
const float COPYRIGHT_S = std::lerp(S::FOOTER_INTRO_SCALE_START, 1.0F, Easing::easeOutQuad(intro_copyright_progress_));
|
const float JAILGAMES_S = dive_zoom_ * std::lerp(S::FOOTER_INTRO_SCALE_START, 1.0F, Easing::easeOutQuad(intro_jailgames_progress_));
|
||||||
|
const float COPYRIGHT_S = dive_zoom_ * std::lerp(S::FOOTER_INTRO_SCALE_START, 1.0F, Easing::easeOutQuad(intro_copyright_progress_));
|
||||||
|
|
||||||
const float JAILGAMES_RENDER_SCALE = Defaults::Title::Layout::JAILGAMES_SCALE * JAILGAMES_S;
|
const float JAILGAMES_RENDER_SCALE = Defaults::Title::Layout::JAILGAMES_SCALE * JAILGAMES_S;
|
||||||
for (const auto& letter : letters_jailgames_) {
|
for (const auto& letter : letters_jailgames_) {
|
||||||
@@ -276,7 +286,7 @@ void TitleScene::dibuixarPeuTitol(float spacing) const {
|
|||||||
.x = SCREEN_CENTRE_X + (JAILGAMES_S * (letter.position.x - SCREEN_CENTRE_X)),
|
.x = SCREEN_CENTRE_X + (JAILGAMES_S * (letter.position.x - SCREEN_CENTRE_X)),
|
||||||
.y = SCREEN_CENTRE_Y + (JAILGAMES_S * (letter.position.y - SCREEN_CENTRE_Y)),
|
.y = SCREEN_CENTRE_Y + (JAILGAMES_S * (letter.position.y - SCREEN_CENTRE_Y)),
|
||||||
};
|
};
|
||||||
Rendering::renderShape(sdl_.getRenderer(), letter.shape, POS, 0.0F, JAILGAMES_RENDER_SCALE, 1.0F, 1.0F, Defaults::Title::Colors::JAILGAMES_LOGO);
|
Rendering::renderShape(sdl_.getRenderer(), letter.shape, POS, 0.0F, JAILGAMES_RENDER_SCALE, 1.0F, Defaults::Title::Layout::JAILGAMES_BRIGHTNESS, Defaults::Title::Colors::JAILGAMES_LOGO);
|
||||||
}
|
}
|
||||||
std::string copyright = Project::COPYRIGHT;
|
std::string copyright = Project::COPYRIGHT;
|
||||||
for (char& c : copyright) {
|
for (char& c : copyright) {
|
||||||
@@ -288,7 +298,7 @@ void TitleScene::dibuixarPeuTitol(float spacing) const {
|
|||||||
const float COPY_X = SCREEN_CENTRE_X; // ja al centre
|
const float COPY_X = SCREEN_CENTRE_X; // ja al centre
|
||||||
const float COPY_Y = SCREEN_CENTRE_Y + (COPYRIGHT_S * (Y_COPY_FINAL - SCREEN_CENTRE_Y));
|
const float COPY_Y = SCREEN_CENTRE_Y + (COPYRIGHT_S * (Y_COPY_FINAL - SCREEN_CENTRE_Y));
|
||||||
const float COPY_RENDER_SCALE = Defaults::Title::Layout::COPYRIGHT_SCALE * COPYRIGHT_S;
|
const float COPY_RENDER_SCALE = Defaults::Title::Layout::COPYRIGHT_SCALE * COPYRIGHT_S;
|
||||||
text_.renderCentered(copyright, {.x = COPY_X, .y = COPY_Y}, COPY_RENDER_SCALE, spacing, 1.0F, Defaults::Title::Colors::COPYRIGHT);
|
text_.renderCentered(copyright, {.x = COPY_X, .y = COPY_Y}, COPY_RENDER_SCALE, spacing, Defaults::Title::Layout::COPYRIGHT_BRIGHTNESS, Defaults::Title::Colors::COPYRIGHT);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto TitleScene::isFinished() const -> bool {
|
auto TitleScene::isFinished() const -> bool {
|
||||||
@@ -296,6 +306,7 @@ auto TitleScene::isFinished() const -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void TitleScene::update(float delta_time) {
|
void TitleScene::update(float delta_time) {
|
||||||
|
blink_timer_ += delta_time; // fase del parpelleig del PRESS START (reiniciada a les transicions)
|
||||||
if (starfield_) {
|
if (starfield_) {
|
||||||
starfield_->update(delta_time);
|
starfield_->update(delta_time);
|
||||||
}
|
}
|
||||||
@@ -324,6 +335,12 @@ void TitleScene::update(float delta_time) {
|
|||||||
case TitleState::BLACK_SCREEN:
|
case TitleState::BLACK_SCREEN:
|
||||||
updateBlackScreenState(delta_time);
|
updateBlackScreenState(delta_time);
|
||||||
break;
|
break;
|
||||||
|
case TitleState::DEMO_DIVE:
|
||||||
|
updateDemoDiveState(delta_time);
|
||||||
|
break;
|
||||||
|
case TitleState::DEMO_CURTAIN:
|
||||||
|
updateDemoCurtainState(delta_time);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Les animacions segueixen pero els inputs es bloquegen mentre el menu
|
// Les animacions segueixen pero els inputs es bloquegen mentre el menu
|
||||||
@@ -340,6 +357,41 @@ void TitleScene::update(float delta_time) {
|
|||||||
handleSkipInput();
|
handleSkipInput();
|
||||||
handleStartInput();
|
handleStartInput();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Attract mode: al state MAIN, acumular inactivitat; qualsevol botó arcade
|
||||||
|
// la reseteja. En esgotar el timeout, saltar a la demo (mode DEMO, P1 actiu)
|
||||||
|
// sense fer fadeOut de la música (a diferència del START real). El comptador
|
||||||
|
// segueix corrent encara que el menu de servei o l'overlay de redefinir
|
||||||
|
// controls estiguen oberts: floten sobre el títol i no aturen el fons, així
|
||||||
|
// que la demo arrenca igualment i les escenes poden canviar amb el menu
|
||||||
|
// obert. L'input, però, no arriba "a baix": només resetegem el comptador amb
|
||||||
|
// un botó arcade quan cap overlay l'està capturant.
|
||||||
|
if (current_state_ == TitleState::MAIN) {
|
||||||
|
if (!INPUT_BLOCKED && Input::get()->checkAnyPlayerAction(ARCADE_BUTTONS, Input::ALLOW_REPEAT)) {
|
||||||
|
idle_timer_ = 0.0F;
|
||||||
|
} else {
|
||||||
|
idle_timer_ += delta_time;
|
||||||
|
}
|
||||||
|
if (idle_timer_ >= TITLE_DEMO_TIMEOUT) {
|
||||||
|
// L'escenari curat (mateix índex que llegirà GameScene) decideix
|
||||||
|
// quants jugadors IA hi ha. No avancem l'índex ací: ho fa GameScene.
|
||||||
|
const Systems::Demo::Scenario SC = Systems::Demo::scenario(context_.demoScenarioIndex());
|
||||||
|
GameConfig::MatchConfig demo_cfg;
|
||||||
|
demo_cfg.player1_active = true;
|
||||||
|
demo_cfg.player2_active = (SC.players >= 2);
|
||||||
|
demo_cfg.mode = GameConfig::Mode::DEMO;
|
||||||
|
context_.setMatchConfig(demo_cfg);
|
||||||
|
// No saltem en sec: primer un "dive" de càmera cap al punt de fuga
|
||||||
|
// (deixa enrere títol/naus/logo) i després la cortinilla. L'estat
|
||||||
|
// deixa de ser MAIN, així que ni es re-dispara la demo ni s'accepta
|
||||||
|
// START durant la transició. Amaguem ja el "PREMEU START".
|
||||||
|
press_start_visible_ = false;
|
||||||
|
current_state_ = TitleState::DEMO_DIVE;
|
||||||
|
dive_time_ = 0.0F;
|
||||||
|
dive_zoom_ = 1.0F;
|
||||||
|
temps_acumulat_ = 0.0F;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void TitleScene::updateStarfieldFadeInState(float delta_time) {
|
void TitleScene::updateStarfieldFadeInState(float delta_time) {
|
||||||
@@ -398,6 +450,7 @@ void TitleScene::updateMainState(float delta_time) {
|
|||||||
|
|
||||||
if (!press_start_visible_ && state_time_main_ >= T_PRESS_START_VISIBLE) {
|
if (!press_start_visible_ && state_time_main_ >= T_PRESS_START_VISIBLE) {
|
||||||
press_start_visible_ = true;
|
press_start_visible_ = true;
|
||||||
|
blink_timer_ = 0.0F; // primer parpelleig (lent) complet en aparèixer
|
||||||
}
|
}
|
||||||
|
|
||||||
// L'oscil·lació suau del logo arrenca quan el logo ha aterrat. Així
|
// L'oscil·lació suau del logo arrenca quan el logo ha aterrat. Així
|
||||||
@@ -442,6 +495,50 @@ void TitleScene::updateBlackScreenState(float delta_time) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TitleScene::advanceDive(float delta_time) {
|
||||||
|
namespace D = Defaults::Game::Dive;
|
||||||
|
dive_time_ += delta_time;
|
||||||
|
// T SENSE topall: easeInQuad (=T²) fa que la posició creixi quadràticament,
|
||||||
|
// és a dir, la velocitat creix linealment → acceleració constant que no
|
||||||
|
// s'atura. CAMERA_DISTANCE i ZOOM_MAX deixen de ser límits: són el valor
|
||||||
|
// assolit en l'instant de referència (T=1, quan cau la cortinilla) i a
|
||||||
|
// partir d'ací el moviment continua accelerant (std::lerp extrapola).
|
||||||
|
const float T = dive_time_ / D::DURATION;
|
||||||
|
const float EASED = Easing::easeInQuad(T);
|
||||||
|
|
||||||
|
// Càmera 3D real cap a +Z: starfield i naus es projecten amb la càmera, així
|
||||||
|
// que les estrelles es rasguen i les naus creixen i s'escapen pels costats.
|
||||||
|
// IMPORTANT: cal moure posició I target alhora; si només es mou la posició,
|
||||||
|
// forward = (target - position) s'inverteix en passar el target i la càmera
|
||||||
|
// gira cap enrere (tot apareix invertit i mirant a l'espectador).
|
||||||
|
if (camera_ != nullptr) {
|
||||||
|
const float Z = EASED * D::CAMERA_DISTANCE;
|
||||||
|
camera_->setPosition(Vec3{.x = 0.0F, .y = 0.0F, .z = Z});
|
||||||
|
camera_->setTarget(Vec3{.x = 0.0F, .y = 0.0F, .z = Z + 1.0F});
|
||||||
|
}
|
||||||
|
// Logo i peu són 2D: els fakegem el dive amb un zoom des del centre.
|
||||||
|
dive_zoom_ = std::lerp(1.0F, D::ZOOM_MAX, EASED);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TitleScene::updateDemoDiveState(float delta_time) {
|
||||||
|
advanceDive(delta_time);
|
||||||
|
|
||||||
|
// En l'instant de referència (dive_time_ == DURATION) cau la cortinilla,
|
||||||
|
// però la càmera NO s'atura: segueix accelerant a DEMO_CURTAIN.
|
||||||
|
if (dive_time_ >= Defaults::Game::Dive::DURATION) {
|
||||||
|
current_state_ = TitleState::DEMO_CURTAIN;
|
||||||
|
curtain_.cover(Defaults::Game::Curtain::COVER_DURATION);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TitleScene::updateDemoCurtainState(float delta_time) {
|
||||||
|
advanceDive(delta_time); // la càmera continua accelerant sota la tela
|
||||||
|
curtain_.update(delta_time);
|
||||||
|
if (curtain_.isDone()) {
|
||||||
|
context_.setNextScene(SceneType::GAME);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void TitleScene::handleSkipInput() {
|
void TitleScene::handleSkipInput() {
|
||||||
if (current_state_ != TitleState::STARFIELD_FADE_IN && current_state_ != TitleState::STARFIELD) {
|
if (current_state_ != TitleState::STARFIELD_FADE_IN && current_state_ != TitleState::STARFIELD) {
|
||||||
return;
|
return;
|
||||||
@@ -465,6 +562,7 @@ void TitleScene::handleSkipInput() {
|
|||||||
intro_jailgames_progress_ = 1.0F;
|
intro_jailgames_progress_ = 1.0F;
|
||||||
intro_copyright_progress_ = 1.0F;
|
intro_copyright_progress_ = 1.0F;
|
||||||
press_start_visible_ = true;
|
press_start_visible_ = true;
|
||||||
|
blink_timer_ = 0.0F; // primer parpelleig (lent) complet en saltar la intro
|
||||||
ships_intro_launched_ = true;
|
ships_intro_launched_ = true;
|
||||||
if (ship_animator_ != nullptr) {
|
if (ship_animator_ != nullptr) {
|
||||||
ship_animator_->setVisible(true);
|
ship_animator_->setVisible(true);
|
||||||
@@ -496,6 +594,7 @@ void TitleScene::handleStartInput() {
|
|||||||
context_.setMatchConfig(match_config_);
|
context_.setMatchConfig(match_config_);
|
||||||
current_state_ = TitleState::PLAYER_JOIN_PHASE;
|
current_state_ = TitleState::PLAYER_JOIN_PHASE;
|
||||||
temps_acumulat_ = 0.0F;
|
temps_acumulat_ = 0.0F;
|
||||||
|
blink_timer_ = 0.0F; // primer parpelleig (ràpid) complet en passar a join phase
|
||||||
|
|
||||||
triggerExitForJoinedPlayers(P1_ABANS, P2_ABANS, "");
|
triggerExitForJoinedPlayers(P1_ABANS, P2_ABANS, "");
|
||||||
|
|
||||||
@@ -546,7 +645,9 @@ void TitleScene::draw() {
|
|||||||
(current_state_ == TitleState::STARFIELD_FADE_IN ||
|
(current_state_ == TitleState::STARFIELD_FADE_IN ||
|
||||||
current_state_ == TitleState::STARFIELD ||
|
current_state_ == TitleState::STARFIELD ||
|
||||||
current_state_ == TitleState::MAIN ||
|
current_state_ == TitleState::MAIN ||
|
||||||
current_state_ == TitleState::PLAYER_JOIN_PHASE)) {
|
current_state_ == TitleState::PLAYER_JOIN_PHASE ||
|
||||||
|
current_state_ == TitleState::DEMO_DIVE ||
|
||||||
|
current_state_ == TitleState::DEMO_CURTAIN)) {
|
||||||
ship_animator_->draw();
|
ship_animator_->draw();
|
||||||
}
|
}
|
||||||
drawFlashes();
|
drawFlashes();
|
||||||
@@ -555,14 +656,21 @@ void TitleScene::draw() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (current_state_ != TitleState::MAIN && current_state_ != TitleState::PLAYER_JOIN_PHASE) {
|
// DEMO_DIVE/DEMO_CURTAIN es pinten com MAIN (logo + peu) perquè el títol
|
||||||
|
// segueixi visible sota el dive i la cortinilla.
|
||||||
|
if (current_state_ != TitleState::MAIN &&
|
||||||
|
current_state_ != TitleState::PLAYER_JOIN_PHASE &&
|
||||||
|
current_state_ != TitleState::DEMO_DIVE &&
|
||||||
|
current_state_ != TitleState::DEMO_CURTAIN) {
|
||||||
|
curtain_.draw(); // BLACK_SCREEN i altres: només la cortinilla (si activa)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Factor d'escala+posició per simular un moviment 3D des de l'usuari (prop,
|
// Factor d'escala+posició per simular un moviment 3D des de l'usuari (prop,
|
||||||
// sprite gran i posició projectada extrema) cap al VP (lluny, sprite a la
|
// sprite gran i posició projectada extrema) cap al VP (lluny, sprite a la
|
||||||
// seva mida i posició finals). Pivot: centre de pantalla (= projecció VP).
|
// seva mida i posició finals). Pivot: centre de pantalla (= projecció VP).
|
||||||
const float LOGO_S = std::lerp(Defaults::Title::Sequence::LOGO_INTRO_SCALE_START, 1.0F, Easing::easeOutQuad(intro_logo_progress_));
|
// El dive (attract) hi afegeix un zoom extra (dive_zoom_) per travessar-los.
|
||||||
|
const float LOGO_S = dive_zoom_ * std::lerp(Defaults::Title::Sequence::LOGO_INTRO_SCALE_START, 1.0F, Easing::easeOutQuad(intro_logo_progress_));
|
||||||
const float SCREEN_CENTRE_X = Defaults::Game::WIDTH / 2.0F;
|
const float SCREEN_CENTRE_X = Defaults::Game::WIDTH / 2.0F;
|
||||||
const float SCREEN_CENTRE_Y = Defaults::Game::HEIGHT / 2.0F;
|
const float SCREEN_CENTRE_Y = Defaults::Game::HEIGHT / 2.0F;
|
||||||
const float LOGO_RENDER_SCALE = Defaults::Title::Layout::LOGO_SCALE * LOGO_S;
|
const float LOGO_RENDER_SCALE = Defaults::Title::Layout::LOGO_SCALE * LOGO_S;
|
||||||
@@ -611,12 +719,14 @@ void TitleScene::draw() {
|
|||||||
const float SPACING = Defaults::Title::Layout::TEXT_SPACING;
|
const float SPACING = Defaults::Title::Layout::TEXT_SPACING;
|
||||||
|
|
||||||
if (press_start_visible_) {
|
if (press_start_visible_) {
|
||||||
bool mostrar_text = true;
|
// Parpelleig: lent en aparèixer (MAIN), ràpid en prémer START (join).
|
||||||
if (current_state_ == TitleState::PLAYER_JOIN_PHASE) {
|
// blink_timer_ es reinicia a 0 a cada transició, així el primer mig
|
||||||
const float FASE = temps_acumulat_ * BLINK_FREQUENCY * 2.0F * std::numbers::pi_v<float>;
|
// període és sempre visible i complet (no n'agafa un de parcial).
|
||||||
mostrar_text = (std::sin(FASE) > 0.0F);
|
const float BLINK_HZ = (current_state_ == TitleState::PLAYER_JOIN_PHASE)
|
||||||
}
|
? Defaults::Title::Layout::PRESS_START_BLINK_HZ_FAST
|
||||||
if (mostrar_text) {
|
: Defaults::Title::Layout::PRESS_START_BLINK_HZ_SLOW;
|
||||||
|
const bool MOSTRAR_TEXT = std::fmod(blink_timer_ * BLINK_HZ, 1.0F) < 0.5F;
|
||||||
|
if (MOSTRAR_TEXT) {
|
||||||
const std::string MAIN_TEXT = Locale::get().text("title.press_start");
|
const std::string MAIN_TEXT = Locale::get().text("title.press_start");
|
||||||
const float MAIN_SCALE = Defaults::Title::Layout::PRESS_START_SCALE;
|
const float MAIN_SCALE = Defaults::Title::Layout::PRESS_START_SCALE;
|
||||||
const float CENTRE_X = Defaults::Game::WIDTH / 2.0F;
|
const float CENTRE_X = Defaults::Game::WIDTH / 2.0F;
|
||||||
@@ -626,6 +736,9 @@ void TitleScene::draw() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dibuixarPeuTitol(SPACING);
|
dibuixarPeuTitol(SPACING);
|
||||||
|
|
||||||
|
// Cortinilla negra (attract): per damunt de tot. No-op si no està activa.
|
||||||
|
curtain_.draw();
|
||||||
}
|
}
|
||||||
|
|
||||||
auto TitleScene::checkSkipButtonPressed() -> bool {
|
auto TitleScene::checkSkipButtonPressed() -> bool {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user