Compare commits

...
10 Commits
22 changed files with 1070 additions and 139 deletions

No files matched your search

+1
View File
@@ -93,6 +93,7 @@ set(APP_SOURCES
source/core/rendering/sprite/animated_sprite.cpp source/core/rendering/sprite/animated_sprite.cpp
source/core/rendering/sprite/animated_sprite_parser.cpp source/core/rendering/sprite/animated_sprite_parser.cpp
source/core/rendering/effects/heat_haze.cpp source/core/rendering/effects/heat_haze.cpp
source/core/rendering/effects/scan_reveal.cpp
source/core/rendering/transitions/mosaic.cpp source/core/rendering/transitions/mosaic.cpp
source/core/rendering/transitions/fade_to_black.cpp source/core/rendering/transitions/fade_to_black.cpp
source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp
+133
View File
@@ -0,0 +1,133 @@
# Font: Tiny5-Regular — generado desde Tiny5-Regular.ttf size 8
# Generado con font_gen.py
box_width 6
box_height 9
columns 15
# codepoint_decimal ancho_visual
32 2 # U+0020
33 1 # !
34 3 # "
35 5 # #
36 3 # $
37 4 # %
38 4 # &
39 1 # '
40 2 # (
41 2 # )
42 3 # *
43 3 # +
44 2 # ,
45 3 # -
46 1 # .
47 2 # /
48 3 # 0
49 2 # 1
50 3 # 2
51 3 # 3
52 3 # 4
53 3 # 5
54 3 # 6
55 3 # 7
56 3 # 8
57 3 # 9
58 1 # :
59 2 # ;
60 3 # <
61 3 # =
62 3 # >
63 3 # ?
64 4 # @
65 4 # A
66 4 # B
67 4 # C
68 4 # D
69 4 # E
70 4 # F
71 4 # G
72 4 # H
73 1 # I
74 3 # J
75 4 # K
76 3 # L
77 5 # M
78 4 # N
79 4 # O
80 4 # P
81 4 # Q
82 4 # R
83 4 # S
84 3 # T
85 4 # U
86 3 # V
87 5 # W
88 3 # X
89 3 # Y
90 3 # Z
91 2 # [
92 2 # \
93 2 # ]
94 3 # ^
95 3 # _
96 2 # `
97 3 # a
98 3 # b
99 3 # c
100 3 # d
101 3 # e
102 2 # f
103 3 # g
104 3 # h
105 1 # i
106 1 # j
107 3 # k
108 1 # l
109 5 # m
110 3 # n
111 3 # o
112 3 # p
113 3 # q
114 2 # r
115 3 # s
116 2 # t
117 3 # u
118 3 # v
119 5 # w
120 3 # x
121 3 # y
122 3 # z
123 3 # {
124 1 # |
125 3 # }
126 2 # ~
192 4 # À
193 4 # Á
200 4 # È
201 4 # É
204 2 # Ì
205 2 # Í
210 4 # Ò
211 4 # Ó
217 4 # Ù
218 4 # Ú
209 4 # Ñ
199 4 # Ç
224 3 # à
225 3 # á
232 3 # è
233 3 # é
236 2 # ì
237 2 # í
242 3 # ò
243 3 # ó
249 3 # ù
250 3 # ú
241 3 # ñ
231 3 # ç
161 1 # ¡
191 3 # ¿
171 5 # «
187 5 # »
183 1 # ·
169 3 # ©
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 17 KiB

+67 -5
View File
@@ -58,6 +58,15 @@ fonts:
gif: "data/fonts/ari-w9500-condensed-display.gif" gif: "data/fonts/ari-w9500-condensed-display.gif"
fnt: "data/fonts/ari-w9500-condensed-display.fnt" fnt: "data/fonts/ari-w9500-condensed-display.fnt"
kerning: 0 kerning: 0
# Copyright de la TITLE: pixel-art estricte (Tiny5-Regular size 5,
# caixa 3×6 px) generat amb font-gen. Diferenciada de la `placeholder`
# que pinta el "press start" — així la jerarquia tipogràfica del peu
# queda neta.
copyright:
name: "Tiny5-Regular"
gif: "data/fonts/Tiny5-Regular.gif"
fnt: "data/fonts/Tiny5-Regular.fnt"
kerning: 1
hud: hud:
bg_enabled: false bg_enabled: false
@@ -101,6 +110,9 @@ sounds:
# Disparat quan un jugador prem START a la TITLE (primera pulsació o # Disparat quan un jugador prem START a la TITLE (primera pulsació o
# la del segon jugador dins de la finestra de STARTING). # la del segon jugador dins de la finestra de STARTING).
player_start: "player_start.wav" player_start: "player_start.wav"
# Soroll/scratch sincronitzat amb la fase SCAN de LogoScene (scanline
# reveal). Es dispara al constructor de l'escena.
logo_glitch: "glitch.wav"
# Escena TITLE (placeholder visual mentre no hi haja artwork). # Escena TITLE (placeholder visual mentre no hi haja artwork).
# `bg_color` i `text_color` són índexs sobre la paleta "ui" (uzebox.pal): # `bg_color` i `text_color` són índexs sobre la paleta "ui" (uzebox.pal):
@@ -113,7 +125,23 @@ title:
duration_ms: 30000 duration_ms: 30000
bg_color: 1 bg_color: 1
text_color: 31 text_color: 31
copyright_color: 246 # Color del text del © de copyright (peu de la TITLE). Off-white càlid:
# índex 183 = (255,218,170), tipus crema/peach, ~90% blanc amb tint càlid.
copyright_color: 183
# Color de l'outline del prompt "PRESS START". Marró fosc càlid (índex
# 19 = 109,72,0), coherent amb la paleta del logo. Es manté separat de
# `copyright_color` perquè l'outline i el text del © tenen rols visuals
# diferents (contrast vs. peu informatiu).
press_start_outline_color: 19
# Posicions Y dels tres elements verticals de la TITLE, en percentatge
# sobre l'alçada del canvas (240 px). El logo s'ancora pel seu top
# (la imatge cau cap avall des d'aquest Y); el prompt i el copyright
# es centren verticalment sobre la línia.
logo_y_percent: 15
prompt_y_percent: 68
copyright_y_percent: 92
# Separació vertical en píxels entre el minilogo i el text del © al peu.
mini_logo_gap_px: 2
blink_visible_ms: 800 blink_visible_ms: 800
blink_hidden_ms: 200 blink_hidden_ms: 200
# Fase STARTING (entre la primera pulsació de START i el salt a la # Fase STARTING (entre la primera pulsació de START i el salt a la
@@ -139,10 +167,20 @@ title:
# mosaic — capa negra lineal damunt del canvas. La música no es toca: # mosaic — capa negra lineal damunt del canvas. La música no es toca:
# `title.ogg` continua sonant cap a LOGO, que ja veurà què en fa. # `title.ogg` continua sonant cap a LOGO, que ja veurà què en fa.
attract_fade_to_black_ms: 800 attract_fade_to_black_ms: 800
# Fade-in inicial: capa negra al començament que s'esvaeix cap al fons. # Seqüència d'aparició dels elements de la TITLE en fase IDLE. Tots
# Mateix valor que `room_intro.fade_in_ms` (data/scenes/game.yaml) perquè # els temps són absoluts des de l'entrada a l'escena. `fade_in_ms` és
# l'arribada a la TITLE i a una sala de joc tinguin el mateix ritme. # la capa negra inicial que s'esvaeix cap al fons; els altres marquen
intro_fade_in_ms: 500 # quan apareix cada element. Si l'usuari prem START en qualsevol
# moment, la seqüència es completa instantàniament.
intro:
fade_in_ms: 500 # fade-in negre inicial sobre el fons
game_logo_at_ms: 1500 # visibilitat binària del game_logo
mini_logo_at_ms: 2000 # inici del slide-in del mini-logo
mini_logo_slide_ms: 500 # durada del slide
copyright_at_ms: 2200 # inici del slide-in del copyright
copyright_slide_ms: 500 # durada del slide
slide_curve: outCubic # corba compartida per mini_logo i copyright
press_start_at_ms: 3000 # quan apareix PRESS START amb parpadeig
# Distorsió sinusoidal del fons (efecte de calor del desert). # Distorsió sinusoidal del fons (efecte de calor del desert).
# `amplitude_px`: desplaçament màxim de cada fila en píxels. # `amplitude_px`: desplaçament màxim de cada fila en píxels.
# `wavelength_px`: distància vertical entre dos pics de l'ona. # `wavelength_px`: distància vertical entre dos pics de l'ona.
@@ -153,3 +191,27 @@ title:
amplitude_px: 1.5 amplitude_px: 1.5
wavelength_px: 32.0 wavelength_px: 32.0
speed_hz: 0.6 speed_hz: 0.6
# Escena LOGO (logotip de l'editora). El revelat del logo a la fase SCAN
# imita un escaneig CRT: una banda lluminosa baixa de dalt a baix; per
# damunt es veu la imatge ja "impressa", per sota encara és negre. Al
# voltant de la banda hi ha glitch (offset horitzontal de files amb
# falloff per distància) i ocasionalment apareixen bursts VHS (franges
# desplaçades) damunt la part ja revelada. Quan la banda toca el peu del
# logo, un flash blanc tanca l'efecte i s'entra a MIDDLE (logo estable).
logo:
scan:
duration_ms: 1500 # durada total del scan top→bottom
band_height_px: 4 # alçada (px) de la banda lluminosa
band_alpha: 200 # opacitat (0..255) de la capa blanca de la banda
band_pulse_hz: 20.0 # freqüència del pulse de brillantor (Hz)
band_pulse_depth: 0.45 # profunditat del pulse (0..1; 0 = sense pulse)
# Glitch d'impacte (files al voltant de la banda)
glitch_amplitude_px: 6 # offset horitzontal màxim a la banda
glitch_band_radius_px: 12 # files damunt/sota la banda amb glitch (falloff lineal)
# Bursts VHS sobre la part ja revelada
vhs_burst_per_sec: 3 # bursts/segon mitjà
vhs_burst_duration_ms: 80 # durada de cada burst
vhs_burst_height_px: 8 # alçada de la franja afectada
vhs_burst_amplitude_px: 8 # offset horitzontal del burst
flash_ms: 100 # flash blanc final quan acaba el scan
@@ -0,0 +1,192 @@
#include "core/rendering/effects/scan_reveal.hpp"
#include <SDL3/SDL.h>
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <numbers>
#include <random>
#include "core/rendering/screen.hpp"
#include "core/rendering/surface.hpp"
namespace Effects {
namespace {
// Hash determinista per a (row, time_bucket): retorna float a
// [-1, 1]. Usat per generar el jitter horitzontal de la banda
// sense necessitat d'un RNG d'estat (cada frame el resultat
// canvia perquè el time_bucket canvia, però dins d'un mateix
// bucket dues files diferents donen valors distints).
auto rowJitter(int row, int time_bucket) -> float {
const auto SEED = static_cast<std::uint32_t>((row * 73856093) ^ (time_bucket * 19349663));
// Mix barato (xorshift) per descorrelacionar bits.
std::uint32_t x = SEED;
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
// Normalitzem a [-1, 1].
const float NORM = static_cast<float>(x) / static_cast<float>(UINT32_MAX);
return (NORM * 2.0F) - 1.0F;
}
// Bucket de temps per al jitter: agrupant en blocs de ~50ms el
// jitter "salta" 20 cops per segon — visualment "viu" sense
// ser caos pur a 60fps.
constexpr int JITTER_TIME_BUCKET_MS = 50;
} // namespace
ScanReveal::ScanReveal()
: rng_(std::random_device{}()) {}
void ScanReveal::begin(const Params& params) {
params_ = params;
elapsed_ms_ = 0;
flash_remaining_ms_ = 0;
flash_started_ = false;
burst_remaining_ms_ = 0;
burst_y_norm_ = 0.0F;
burst_dx_ = 0;
}
void ScanReveal::tick(int delta_ms) {
if (params_.duration_ms <= 0) { return; }
// 1) Avanç del scan (clampejat a duration_ms).
elapsed_ms_ = std::min(elapsed_ms_ + delta_ms, params_.duration_ms);
// 2) Tick del burst VHS actiu, si n'hi ha.
if (burst_remaining_ms_ > 0) {
burst_remaining_ms_ = std::max(0, burst_remaining_ms_ - delta_ms);
}
// 3) Spawn d'un nou burst (només durant el scan, no al flash).
// Probabilitat per frame ≈ (per_sec/1000) · delta_ms.
if (burst_remaining_ms_ == 0 && elapsed_ms_ < params_.duration_ms && params_.vhs_burst_per_sec > 0 && params_.vhs_burst_duration_ms > 0) {
const float P = (static_cast<float>(params_.vhs_burst_per_sec) / 1000.0F) * static_cast<float>(delta_ms);
std::uniform_real_distribution<float> dist01(0.0F, 1.0F);
if (dist01(rng_) < P) {
burst_remaining_ms_ = params_.vhs_burst_duration_ms;
burst_y_norm_ = dist01(rng_);
std::uniform_int_distribution<int> dist_sign(0, 1);
const int SIGN = (dist_sign(rng_) == 0) ? 1 : -1;
burst_dx_ = SIGN * params_.vhs_burst_amplitude_px;
}
}
// 4) Dispara el flash quan el scan acaba (una sola vegada).
if (!flash_started_ && elapsed_ms_ >= params_.duration_ms && params_.flash_ms > 0) {
flash_started_ = true;
flash_remaining_ms_ = params_.flash_ms;
}
// Decrement del flash mentre estiga actiu.
if (flash_remaining_ms_ > 0) {
flash_remaining_ms_ = std::max(0, flash_remaining_ms_ - delta_ms);
}
}
auto ScanReveal::isActive() const -> bool {
return params_.duration_ms > 0 && !isFinished();
}
auto ScanReveal::isFinished() const -> bool {
if (params_.duration_ms <= 0) { return true; }
// Acabat = scan al final + flash exhaurit (o sense flash).
return elapsed_ms_ >= params_.duration_ms && flash_remaining_ms_ <= 0;
}
void ScanReveal::render(const Surface& src, int x, int y) const {
if (params_.duration_ms <= 0) {
// Sense efecte: render pla.
src.render(x, y);
return;
}
const int H = src.getHeight();
const auto W_F = static_cast<float>(src.getWidth());
const float T = static_cast<float>(elapsed_ms_) / static_cast<float>(params_.duration_ms);
// Banda: comença a y=-band_height i acaba a y=H. Així al t=0 cap
// fila és visible i al t=1 totes ho són.
const float SWEEP_F = -static_cast<float>(params_.band_height_px) + (T * static_cast<float>(H + params_.band_height_px));
const int BAND_TOP = static_cast<int>(std::floor(SWEEP_F));
const int BAND_BOT = BAND_TOP + params_.band_height_px;
const int TIME_BUCKET = elapsed_ms_ / JITTER_TIME_BUCKET_MS;
const int BURST_Y_PX = static_cast<int>(burst_y_norm_ * static_cast<float>(H));
const bool BURST_ACTIVE = burst_remaining_ms_ > 0;
// Render fila a fila (només files revelades).
for (int row = 0; row <= BAND_BOT && row < H; ++row) {
int dx = 0;
// Jitter de banda: amplitud màxima a la banda, falloff
// lineal cap als marges del radi de glitch.
const int DIST_TO_BAND = std::abs(row - BAND_TOP);
if (params_.glitch_band_radius_px > 0 && DIST_TO_BAND <= params_.glitch_band_radius_px && params_.glitch_amplitude_px > 0) {
const float FALLOFF = 1.0F - (static_cast<float>(DIST_TO_BAND) / static_cast<float>(params_.glitch_band_radius_px));
const float JIT = rowJitter(row, TIME_BUCKET);
dx += static_cast<int>(std::lround(
FALLOFF * static_cast<float>(params_.glitch_amplitude_px) * JIT));
}
// Burst VHS: només damunt la part ja revelada (row < BAND_TOP)
// perquè per sota encara no hi ha imatge "estable" que poder
// distorsionar visualment.
if (BURST_ACTIVE && row < BAND_TOP && row >= BURST_Y_PX && row < BURST_Y_PX + params_.vhs_burst_height_px) {
dx += burst_dx_;
}
SDL_FRect row_rect{0.0F, static_cast<float>(row), W_F, 1.0F};
src.render(x + dx, y + row, &row_rect);
}
auto& screen = Screen::get();
// Capa lluminosa de la banda. Verticalment només cobreix les
// files del logo (és on viu el scan), però horitzontalment
// travessa tot el canvas com un feix CRT, així sobre fons negre
// no es veuen les costures del rectangle del Surface. L'alpha
// base es modula amb un pulse sinusoidal a `band_pulse_hz` per
// simular el ritme de refresc d'un CRT.
if (params_.band_alpha > 0 && params_.band_height_px > 0) {
const int VISIBLE_TOP = std::clamp(BAND_TOP, 0, H);
const int VISIBLE_BOT = std::clamp(BAND_BOT, 0, H);
const int VISIBLE_H = VISIBLE_BOT - VISIBLE_TOP;
if (VISIBLE_H > 0) {
// Pulse: fracció ∈ [0,1] avançada a la freqüència donada,
// mapejada a un multiplicador ∈ [1-depth, 1] sobre l'alpha
// base. Amb depth=0 el pulse és inactiu (multiplicador 1).
const float PULSE_DEPTH = std::clamp(params_.band_pulse_depth, 0.0F, 1.0F);
const float TWO_PI = 2.0F * std::numbers::pi_v<float>;
const float PHASE = TWO_PI * params_.band_pulse_hz * (static_cast<float>(elapsed_ms_) / 1000.0F);
const float WAVE = 0.5F + (0.5F * std::sin(PHASE)); // 0..1
const float MULT = (1.0F - PULSE_DEPTH) + (PULSE_DEPTH * WAVE);
const float ALPHA_F = static_cast<float>(params_.band_alpha) * MULT;
const auto ALPHA = static_cast<std::uint8_t>(
std::clamp(static_cast<int>(std::round(ALPHA_F)), 0, 255));
if (ALPHA > 0) {
screen.blendRect(0, y + VISIBLE_TOP, screen.getCanvasWidth(), VISIBLE_H, 0xFFFFFFFFU, ALPHA);
}
}
}
// Flash final: capa blanca damunt de TOT el canvas (no només
// del rectangle del logo) — així el flash banya l'escena
// sencera com un CRT i no es veuen les costures del Surface
// sobre fons negre. Alpha proporcional a `flash_remaining_ms_
// / flash_ms` (decreix suau cap a 0 al final del flash).
if (flash_remaining_ms_ > 0 && params_.flash_ms > 0) {
const float FLASH_T = static_cast<float>(flash_remaining_ms_) / static_cast<float>(params_.flash_ms);
const auto ALPHA = static_cast<std::uint8_t>(
std::round(255.0F * std::clamp(FLASH_T, 0.0F, 1.0F)));
if (ALPHA > 0) {
screen.blendRect(0, 0, screen.getCanvasWidth(), screen.getCanvasHeight(), 0xFFFFFFFFU, ALPHA);
}
}
}
} // namespace Effects
@@ -0,0 +1,83 @@
#pragma once
#include <cstdint>
#include <random>
class Surface;
namespace Effects {
// Revelat tipus "scanline CRT" d'una Surface. Una banda lluminosa
// baixa des de dalt cap a baix de la imatge: les files damunt de la
// banda es renderitzen normalment (ja revelades), les files sota
// queden invisibles, i a la banda mateixa s'aplica un overlay
// blanc semi-transparent. Al voltant de la banda les files reben un
// jitter horitzontal aleatori (falloff lineal amb la distància) i
// damunt la part ja revelada apareixen ocasionalment "bursts VHS" —
// franges horitzontals desplaçades durant uns ms — per a un toc
// demoscene/VHS deteriorat.
//
// Quan la banda surt per sota del logo i s'esgota `duration_ms`,
// dispara un flash blanc breu i passa a `isFinished()=true`. El
// caller decideix què fa amb el control de fase a partir d'aquí.
//
// Ús:
// ScanReveal sr;
// sr.begin({.duration_ms = 1500, ...});
// // cada frame:
// sr.tick(delta_ms);
// sr.render(*logo, x, y);
// if (sr.isFinished()) { ... canvi de fase ... }
class ScanReveal {
public:
struct Params {
int duration_ms;
int band_height_px;
int band_alpha;
float band_pulse_hz;
float band_pulse_depth;
int glitch_amplitude_px;
int glitch_band_radius_px;
int vhs_burst_per_sec;
int vhs_burst_duration_ms;
int vhs_burst_height_px;
int vhs_burst_amplitude_px;
int flash_ms;
};
ScanReveal();
void begin(const Params& params);
void tick(int delta_ms);
// Renderitza un frame del revelat sobre la Surface `src`. El
// caller s'encarrega de pintar negre per sota (les files no
// revelades simplement no es dibuixen — queda el que hi haja al
// canvas, normalment negre).
void render(const Surface& src, int x, int y) const;
[[nodiscard]] auto isActive() const -> bool;
[[nodiscard]] auto isFinished() const -> bool;
private:
Params params_{};
int elapsed_ms_{0};
// Flash post-scan: arranca quan el scan toca fons; decreix amb
// `delta_ms` fins a 0. Mentre `flash_active_` és true encara no
// hem reportat finished (donant marge per pintar el flash sencer).
int flash_remaining_ms_{0};
bool flash_started_{false};
// Burst VHS actiu (com a màxim un alhora — simplifica i ja és
// suficient per al ritme visual). y_norm ∈ [0,1] es resol en
// píxels al `render()` perquè aquí no coneixem l'alçada del logo.
int burst_remaining_ms_{0};
float burst_y_norm_{0.0F};
int burst_dx_{0};
// RNG per als bursts. Seed des de random_device a `begin()`
// perquè cada execució sigui visualment diferent.
std::minstd_rand rng_;
};
} // namespace Effects
+43 -4
View File
@@ -170,6 +170,10 @@ Director::Director() {
// prem START (vore enterStarting i el reset del segon jugador). // prem START (vore enterStarting i el reset del segon jugador).
const auto& player_start = UiConfig::get().playerStartSound(); const auto& player_start = UiConfig::get().playerStartSound();
Resource::Cache::get().loadSound(player_start, "data/sounds/" + player_start); Resource::Cache::get().loadSound(player_start, "data/sounds/" + player_start);
// Soroll de LogoScene (scanline reveal). El dispara LogoScene al
// ctor; amb fallback al nom de Defaults si la clau YAML no hi és.
const auto& logo_glitch = UiConfig::get().logoGlitchSound();
Resource::Cache::get().loadSound(logo_glitch, "data/sounds/" + logo_glitch);
// Catàleg de presets d'efectes per als sons. Es carrega abans d'Audio // Catàleg de presets d'efectes per als sons. Es carrega abans d'Audio
// perquè els primers playSoundWith* (al començament del joc) ja // perquè els primers playSoundWith* (al començament del joc) ja
@@ -200,6 +204,16 @@ Director::Director() {
placeholder_text->setDefaultKerning(UiConfig::get().placeholderFont().kerning); placeholder_text->setDefaultKerning(UiConfig::get().placeholderFont().kerning);
placeholder_text_ = placeholder_text; placeholder_text_ = placeholder_text;
// Font del copyright de la TITLE (Tiny5-Regular size 8). Pixel-art
// estricte; va amb la paleta UI per compartir índex de color amb la
// resta de textos placeholder.
const std::string COPYRIGHT_FONT_GIF_KEY = UiConfig::get().copyrightFont().name + ".gif";
Resource::Cache::get().loadSurface(COPYRIGHT_FONT_GIF_KEY, UiConfig::get().copyrightFont().gif);
Resource::Cache::get().getSurface(COPYRIGHT_FONT_GIF_KEY)->setPalette(Resource::Cache::get().getPalette("ui"));
auto copyright_text = std::make_shared<Text>(Resource::Cache::get().getSurface(COPYRIGHT_FONT_GIF_KEY), UiConfig::get().copyrightFont().fnt);
copyright_text->setDefaultKerning(UiConfig::get().copyrightFont().kerning);
copyright_text_ = copyright_text;
// Font del menú de servei (F9). El títol va amb la variant `display` // Font del menú de servei (F9). El títol va amb la variant `display`
// i els items amb `condensed-display` (més compacte perquè caben més // i els items amb `condensed-display` (més compacte perquè caben més
// files). Els dos noms i kernings viuen a data/ui.yaml via UiConfig. // files). Els dos noms i kernings viuen a data/ui.yaml via UiConfig.
@@ -213,7 +227,6 @@ Director::Director() {
}; };
auto menu_title_text = load_menu_font(UiConfig::get().menuTitleFont()); auto menu_title_text = load_menu_font(UiConfig::get().menuTitleFont());
auto menu_items_text = load_menu_font(UiConfig::get().menuItemsFont()); auto menu_items_text = load_menu_font(UiConfig::get().menuItemsFont());
menu_title_text_ = menu_title_text;
// Mini-logo del peu de la TITLE. Es preloda ací per consistència // Mini-logo del peu de la TITLE. Es preloda ací per consistència
// amb la resta d'assets d'UI; manté la seua paleta original (no la // amb la resta d'assets d'UI; manté la seua paleta original (no la
@@ -224,6 +237,20 @@ Director::Director() {
auto mini_logo_surface = Resource::Cache::get().getSurface(MINI_LOGO_KEY); auto mini_logo_surface = Resource::Cache::get().getSurface(MINI_LOGO_KEY);
mini_logo_surface_ = mini_logo_surface; mini_logo_surface_ = mini_logo_surface;
// Logo sencer per a l'escena LOGO (centrat damunt fons negre). També
// conserva la seua paleta nadiua: els colors finals del logo viuen
// dins del propi GIF i no han de passar per la paleta UI.
constexpr const char* LOGO_FULL_KEY = "logo_jailgames.gif";
Resource::Cache::get().loadSurface(LOGO_FULL_KEY, "data/logo/logo_jailgames.gif");
logo_full_surface_ = Resource::Cache::get().getSurface(LOGO_FULL_KEY);
// Logotip del joc per a la TITLE (substitueix el text "Aventures en
// Egipte ARCADE"). Igual que la resta de logos, paleta nadiua —
// l'art ja porta els colors finals i no passa per la UI.
constexpr const char* GAME_LOGO_KEY = "game_logo.gif";
Resource::Cache::get().loadSurface(GAME_LOGO_KEY, "data/title/game_logo.gif");
game_logo_surface_ = Resource::Cache::get().getSurface(GAME_LOGO_KEY);
// Fons sencer de la TITLE (320×240). Igual que el minilogo, conserva // Fons sencer de la TITLE (320×240). Igual que el minilogo, conserva
// la seua paleta nadiua perquè el GIF ja porta els colors finals. // la seua paleta nadiua perquè el GIF ja porta els colors finals.
constexpr const char* TITLE_BG_KEY = "title_bg.gif"; constexpr const char* TITLE_BG_KEY = "title_bg.gif";
@@ -393,6 +420,16 @@ void Director::processMenuInput() {
void Director::switchSceneIfRequested() { void Director::switchSceneIfRequested() {
if (!current_scene_) { return; } if (!current_scene_) { return; }
// Prioritat al menú: si un item del ServiceMenu ha encuat una
// transició (típicament "Reiniciar" → LOGO amb reset), s'aplica
// primer. Substitueix l'`ExitRequest` que pugui haver publicat
// l'escena en aquest mateix frame — l'usuari ha cancel·lat l'estat
// de joc explícitament i no ens interessa mantenir-ne la sortida.
if (auto menu_req = ServiceMenu::get().takePendingSceneRequest(); menu_req.has_value()) {
std::cout << "Director: scene transition (menu) → " << toString(menu_req->next) << '\n';
current_scene_ = buildScene(menu_req->next, menu_req->params);
return;
}
// L'escena ha publicat un `ExitRequest` dins de `exit_request_`. // L'escena ha publicat un `ExitRequest` dins de `exit_request_`.
// Director el desempaqueta i construeix la nova escena via // Director el desempaqueta i construeix la nova escena via
// `buildScene`. Si la `std::any_cast` falla (tipus inesperat), el // `buildScene`. Si la `std::any_cast` falla (tipus inesperat), el
@@ -415,10 +452,12 @@ auto Director::buildScene(SceneId id, const SceneParams& params) -> std::unique_
// ctors antics no l'utilitzen directament. Quan totes estiguen // ctors antics no l'utilitzen directament. Quan totes estiguen
// migrades a `ExitRequest`, aquestes lambdes desapareixeran. // migrades a `ExitRequest`, aquestes lambdes desapareixeran.
switch (id) { switch (id) {
case SceneId::LOGO: case SceneId::LOGO: {
return std::make_unique<LogoScene>(placeholder_text_); const auto& p = std::get<LogoParams>(params);
return std::make_unique<LogoScene>(logo_full_surface_, p.reset);
}
case SceneId::TITLE: case SceneId::TITLE:
return std::make_unique<TitleScene>(menu_title_text_, placeholder_text_, title_bg_surface_, mini_logo_surface_); return std::make_unique<TitleScene>(placeholder_text_, copyright_text_, title_bg_surface_, game_logo_surface_, mini_logo_surface_);
case SceneId::INTRO: { case SceneId::INTRO: {
const auto& p = std::get<IntroParams>(params); const auto& p = std::get<IntroParams>(params);
return std::make_unique<IntroScene>(placeholder_text_, p.starting_players); return std::make_unique<IntroScene>(placeholder_text_, p.starting_players);
+3 -1
View File
@@ -30,9 +30,11 @@ class Director {
// escena. No són `static`/Meyers perquè depenen de l'inici d'SDL i // escena. No són `static`/Meyers perquè depenen de l'inici d'SDL i
// de Resource::Cache. // de Resource::Cache.
std::shared_ptr<Text> placeholder_text_; std::shared_ptr<Text> placeholder_text_;
std::shared_ptr<Text> menu_title_text_; std::shared_ptr<Text> copyright_text_;
std::shared_ptr<Surface> title_bg_surface_; std::shared_ptr<Surface> title_bg_surface_;
std::shared_ptr<Surface> game_logo_surface_;
std::shared_ptr<Surface> mini_logo_surface_; std::shared_ptr<Surface> mini_logo_surface_;
std::shared_ptr<Surface> logo_full_surface_;
void createSystemFolder(); void createSystemFolder();
void processEvents(); void processEvents();
+68 -2
View File
@@ -100,6 +100,11 @@ UiConfig::UiConfig()
title_bg_color_(Defaults::UI::Title::BG_COLOR), title_bg_color_(Defaults::UI::Title::BG_COLOR),
title_text_color_(Defaults::UI::Title::TEXT_COLOR), title_text_color_(Defaults::UI::Title::TEXT_COLOR),
title_copyright_color_(Defaults::UI::Title::COPYRIGHT_COLOR), title_copyright_color_(Defaults::UI::Title::COPYRIGHT_COLOR),
title_press_start_outline_color_(Defaults::UI::Title::PRESS_START_OUTLINE_COLOR),
title_logo_y_percent_(Defaults::UI::Title::LOGO_Y_PERCENT),
title_prompt_y_percent_(Defaults::UI::Title::PROMPT_Y_PERCENT),
title_copyright_y_percent_(Defaults::UI::Title::COPYRIGHT_Y_PERCENT),
title_mini_logo_gap_px_(Defaults::UI::Title::MINI_LOGO_GAP_PX),
title_blink_visible_ms_(Defaults::UI::Title::BLINK_VISIBLE_MS), title_blink_visible_ms_(Defaults::UI::Title::BLINK_VISIBLE_MS),
title_blink_hidden_ms_(Defaults::UI::Title::BLINK_HIDDEN_MS), title_blink_hidden_ms_(Defaults::UI::Title::BLINK_HIDDEN_MS),
title_starting_ms_(Defaults::UI::Title::STARTING_MS), title_starting_ms_(Defaults::UI::Title::STARTING_MS),
@@ -108,21 +113,46 @@ UiConfig::UiConfig()
title_fade_out_max_block_(Defaults::UI::Title::FADE_OUT_MAX_BLOCK), title_fade_out_max_block_(Defaults::UI::Title::FADE_OUT_MAX_BLOCK),
title_music_fade_out_ms_(Defaults::UI::Title::MUSIC_FADE_OUT_MS), title_music_fade_out_ms_(Defaults::UI::Title::MUSIC_FADE_OUT_MS),
title_attract_fade_to_black_ms_(Defaults::UI::Title::ATTRACT_FADE_TO_BLACK_MS), title_attract_fade_to_black_ms_(Defaults::UI::Title::ATTRACT_FADE_TO_BLACK_MS),
title_intro_fade_in_ms_(Defaults::UI::Title::INTRO_FADE_IN_MS), title_intro_{
.fade_in_ms = Defaults::UI::Title::Intro::FADE_IN_MS,
.game_logo_at_ms = Defaults::UI::Title::Intro::GAME_LOGO_AT_MS,
.mini_logo_at_ms = Defaults::UI::Title::Intro::MINI_LOGO_AT_MS,
.mini_logo_slide_ms = Defaults::UI::Title::Intro::MINI_LOGO_SLIDE_MS,
.copyright_at_ms = Defaults::UI::Title::Intro::COPYRIGHT_AT_MS,
.copyright_slide_ms = Defaults::UI::Title::Intro::COPYRIGHT_SLIDE_MS,
.slide_curve = curveByName(Defaults::UI::Title::Intro::SLIDE_CURVE),
.press_start_at_ms = Defaults::UI::Title::Intro::PRESS_START_AT_MS,
},
title_heat_haze_{ title_heat_haze_{
.enabled = Defaults::UI::Title::HeatHaze::ENABLED, .enabled = Defaults::UI::Title::HeatHaze::ENABLED,
.amplitude_px = Defaults::UI::Title::HeatHaze::AMPLITUDE_PX, .amplitude_px = Defaults::UI::Title::HeatHaze::AMPLITUDE_PX,
.wavelength_px = Defaults::UI::Title::HeatHaze::WAVELENGTH_PX, .wavelength_px = Defaults::UI::Title::HeatHaze::WAVELENGTH_PX,
.speed_hz = Defaults::UI::Title::HeatHaze::SPEED_HZ, .speed_hz = Defaults::UI::Title::HeatHaze::SPEED_HZ,
}, },
logo_scan_{
.duration_ms = Defaults::UI::Logo::Scan::DURATION_MS,
.band_height_px = Defaults::UI::Logo::Scan::BAND_HEIGHT_PX,
.band_alpha = Defaults::UI::Logo::Scan::BAND_ALPHA,
.band_pulse_hz = Defaults::UI::Logo::Scan::BAND_PULSE_HZ,
.band_pulse_depth = Defaults::UI::Logo::Scan::BAND_PULSE_DEPTH,
.glitch_amplitude_px = Defaults::UI::Logo::Scan::GLITCH_AMPLITUDE_PX,
.glitch_band_radius_px = Defaults::UI::Logo::Scan::GLITCH_BAND_RADIUS_PX,
.vhs_burst_per_sec = Defaults::UI::Logo::Scan::VHS_BURST_PER_SEC,
.vhs_burst_duration_ms = Defaults::UI::Logo::Scan::VHS_BURST_DURATION_MS,
.vhs_burst_height_px = Defaults::UI::Logo::Scan::VHS_BURST_HEIGHT_PX,
.vhs_burst_amplitude_px = Defaults::UI::Logo::Scan::VHS_BURST_AMPLITUDE_PX,
.flash_ms = Defaults::UI::Logo::Scan::FLASH_MS,
},
menu_select_sound_(Defaults::UI::MENU_SELECT_SOUND), menu_select_sound_(Defaults::UI::MENU_SELECT_SOUND),
menu_accept_sound_(Defaults::UI::MENU_ACCEPT_SOUND), menu_accept_sound_(Defaults::UI::MENU_ACCEPT_SOUND),
player_start_sound_(Defaults::UI::PLAYER_START_SOUND), player_start_sound_(Defaults::UI::PLAYER_START_SOUND),
logo_glitch_sound_(Defaults::UI::LOGO_GLITCH_SOUND),
hud_font_{.name = Defaults::UI::Font::HUD_NAME, .gif = Defaults::UI::Font::HUD_GIF, .fnt = Defaults::UI::Font::HUD_FNT, .kerning = Defaults::UI::Font::HUD_KERNING}, hud_font_{.name = Defaults::UI::Font::HUD_NAME, .gif = Defaults::UI::Font::HUD_GIF, .fnt = Defaults::UI::Font::HUD_FNT, .kerning = Defaults::UI::Font::HUD_KERNING},
placeholder_font_{.name = Defaults::UI::Font::PLACEHOLDER_NAME, .gif = Defaults::UI::Font::PLACEHOLDER_GIF, .fnt = Defaults::UI::Font::PLACEHOLDER_FNT, .kerning = Defaults::UI::Font::PLACEHOLDER_KERNING}, placeholder_font_{.name = Defaults::UI::Font::PLACEHOLDER_NAME, .gif = Defaults::UI::Font::PLACEHOLDER_GIF, .fnt = Defaults::UI::Font::PLACEHOLDER_FNT, .kerning = Defaults::UI::Font::PLACEHOLDER_KERNING},
room_text_font_{.name = Defaults::UI::Font::ROOM_TEXT_NAME, .gif = Defaults::UI::Font::ROOM_TEXT_GIF, .fnt = Defaults::UI::Font::ROOM_TEXT_FNT, .kerning = Defaults::UI::Font::ROOM_TEXT_KERNING}, room_text_font_{.name = Defaults::UI::Font::ROOM_TEXT_NAME, .gif = Defaults::UI::Font::ROOM_TEXT_GIF, .fnt = Defaults::UI::Font::ROOM_TEXT_FNT, .kerning = Defaults::UI::Font::ROOM_TEXT_KERNING},
menu_title_font_{.name = Defaults::UI::Font::MENU_TITLE_NAME, .gif = Defaults::UI::Font::MENU_TITLE_GIF, .fnt = Defaults::UI::Font::MENU_TITLE_FNT, .kerning = Defaults::UI::Font::MENU_TITLE_KERNING}, menu_title_font_{.name = Defaults::UI::Font::MENU_TITLE_NAME, .gif = Defaults::UI::Font::MENU_TITLE_GIF, .fnt = Defaults::UI::Font::MENU_TITLE_FNT, .kerning = Defaults::UI::Font::MENU_TITLE_KERNING},
menu_items_font_{.name = Defaults::UI::Font::MENU_ITEMS_NAME, .gif = Defaults::UI::Font::MENU_ITEMS_GIF, .fnt = Defaults::UI::Font::MENU_ITEMS_FNT, .kerning = Defaults::UI::Font::MENU_ITEMS_KERNING}, menu_items_font_{.name = Defaults::UI::Font::MENU_ITEMS_NAME, .gif = Defaults::UI::Font::MENU_ITEMS_GIF, .fnt = Defaults::UI::Font::MENU_ITEMS_FNT, .kerning = Defaults::UI::Font::MENU_ITEMS_KERNING},
copyright_font_{.name = Defaults::UI::Font::COPYRIGHT_NAME, .gif = Defaults::UI::Font::COPYRIGHT_GIF, .fnt = Defaults::UI::Font::COPYRIGHT_FNT, .kerning = Defaults::UI::Font::COPYRIGHT_KERNING},
notifier_{ notifier_{
.padding_h = Defaults::UI::Notifier::PADDING_H, .padding_h = Defaults::UI::Notifier::PADDING_H,
.padding_top = Defaults::UI::Notifier::PADDING_TOP, .padding_top = Defaults::UI::Notifier::PADDING_TOP,
@@ -200,6 +230,7 @@ void UiConfig::load(const std::string& file_path) {
readFont(f, "room_text", room_text_font_); readFont(f, "room_text", room_text_font_);
readFont(f, "menu_title", menu_title_font_); readFont(f, "menu_title", menu_title_font_);
readFont(f, "menu_items", menu_items_font_); readFont(f, "menu_items", menu_items_font_);
readFont(f, "copyright", copyright_font_);
} }
if (yaml.contains("hud")) { if (yaml.contains("hud")) {
@@ -223,6 +254,7 @@ void UiConfig::load(const std::string& file_path) {
readField(s, "menu_select", menu_select_sound_); readField(s, "menu_select", menu_select_sound_);
readField(s, "menu_accept", menu_accept_sound_); readField(s, "menu_accept", menu_accept_sound_);
readField(s, "player_start", player_start_sound_); readField(s, "player_start", player_start_sound_);
readField(s, "logo_glitch", logo_glitch_sound_);
} }
if (yaml.contains("title")) { if (yaml.contains("title")) {
@@ -231,6 +263,11 @@ void UiConfig::load(const std::string& file_path) {
readColorIndex(t, "bg_color", title_bg_color_); readColorIndex(t, "bg_color", title_bg_color_);
readColorIndex(t, "text_color", title_text_color_); readColorIndex(t, "text_color", title_text_color_);
readColorIndex(t, "copyright_color", title_copyright_color_); readColorIndex(t, "copyright_color", title_copyright_color_);
readColorIndex(t, "press_start_outline_color", title_press_start_outline_color_);
readField(t, "logo_y_percent", title_logo_y_percent_);
readField(t, "prompt_y_percent", title_prompt_y_percent_);
readField(t, "copyright_y_percent", title_copyright_y_percent_);
readField(t, "mini_logo_gap_px", title_mini_logo_gap_px_);
readField(t, "blink_visible_ms", title_blink_visible_ms_); readField(t, "blink_visible_ms", title_blink_visible_ms_);
readField(t, "blink_hidden_ms", title_blink_hidden_ms_); readField(t, "blink_hidden_ms", title_blink_hidden_ms_);
readField(t, "starting_ms", title_starting_ms_); readField(t, "starting_ms", title_starting_ms_);
@@ -239,7 +276,17 @@ void UiConfig::load(const std::string& file_path) {
readField(t, "fade_out_max_block", title_fade_out_max_block_); readField(t, "fade_out_max_block", title_fade_out_max_block_);
readField(t, "music_fade_out_ms", title_music_fade_out_ms_); readField(t, "music_fade_out_ms", title_music_fade_out_ms_);
readField(t, "attract_fade_to_black_ms", title_attract_fade_to_black_ms_); readField(t, "attract_fade_to_black_ms", title_attract_fade_to_black_ms_);
readField(t, "intro_fade_in_ms", title_intro_fade_in_ms_); if (t.contains("intro")) {
const auto& in = t["intro"];
readField(in, "fade_in_ms", title_intro_.fade_in_ms);
readField(in, "game_logo_at_ms", title_intro_.game_logo_at_ms);
readField(in, "mini_logo_at_ms", title_intro_.mini_logo_at_ms);
readField(in, "mini_logo_slide_ms", title_intro_.mini_logo_slide_ms);
readField(in, "copyright_at_ms", title_intro_.copyright_at_ms);
readField(in, "copyright_slide_ms", title_intro_.copyright_slide_ms);
readCurve(in, "slide_curve", title_intro_.slide_curve);
readField(in, "press_start_at_ms", title_intro_.press_start_at_ms);
}
if (t.contains("heat_haze")) { if (t.contains("heat_haze")) {
const auto& hh = t["heat_haze"]; const auto& hh = t["heat_haze"];
readField(hh, "enabled", title_heat_haze_.enabled); readField(hh, "enabled", title_heat_haze_.enabled);
@@ -249,6 +296,25 @@ void UiConfig::load(const std::string& file_path) {
} }
} }
if (yaml.contains("logo")) {
const auto& l = yaml["logo"];
if (l.contains("scan")) {
const auto& s = l["scan"];
readField(s, "duration_ms", logo_scan_.duration_ms);
readField(s, "band_height_px", logo_scan_.band_height_px);
readField(s, "band_alpha", logo_scan_.band_alpha);
readField(s, "band_pulse_hz", logo_scan_.band_pulse_hz);
readField(s, "band_pulse_depth", logo_scan_.band_pulse_depth);
readField(s, "glitch_amplitude_px", logo_scan_.glitch_amplitude_px);
readField(s, "glitch_band_radius_px", logo_scan_.glitch_band_radius_px);
readField(s, "vhs_burst_per_sec", logo_scan_.vhs_burst_per_sec);
readField(s, "vhs_burst_duration_ms", logo_scan_.vhs_burst_duration_ms);
readField(s, "vhs_burst_height_px", logo_scan_.vhs_burst_height_px);
readField(s, "vhs_burst_amplitude_px", logo_scan_.vhs_burst_amplitude_px);
readField(s, "flash_ms", logo_scan_.flash_ms);
}
}
if (yaml.contains("notifier")) { if (yaml.contains("notifier")) {
const auto& n = yaml["notifier"]; const auto& n = yaml["notifier"];
readField(n, "padding_h", notifier_.padding_h); readField(n, "padding_h", notifier_.padding_h);
+50 -2
View File
@@ -89,10 +89,29 @@ class UiConfig {
float speed_hz; float speed_hz;
}; };
// Seqüència d'aparició dels elements en fase IDLE: tots els temps
// són absoluts des de l'entrada a l'escena. `slide_curve` es
// comparteix entre el slide-in del mini-logo i el del copyright.
struct TitleIntro {
int fade_in_ms;
int game_logo_at_ms;
int mini_logo_at_ms;
int mini_logo_slide_ms;
int copyright_at_ms;
int copyright_slide_ms;
CurveFn slide_curve;
int press_start_at_ms;
};
[[nodiscard]] auto titleDurationMs() const -> int { return title_duration_ms_; } [[nodiscard]] auto titleDurationMs() const -> int { return title_duration_ms_; }
[[nodiscard]] auto titleBgColor() const -> Uint8 { return title_bg_color_; } [[nodiscard]] auto titleBgColor() const -> Uint8 { return title_bg_color_; }
[[nodiscard]] auto titleTextColor() const -> Uint8 { return title_text_color_; } [[nodiscard]] auto titleTextColor() const -> Uint8 { return title_text_color_; }
[[nodiscard]] auto titleCopyrightColor() const -> Uint8 { return title_copyright_color_; } [[nodiscard]] auto titleCopyrightColor() const -> Uint8 { return title_copyright_color_; }
[[nodiscard]] auto titlePressStartOutlineColor() const -> Uint8 { return title_press_start_outline_color_; }
[[nodiscard]] auto titleLogoYPercent() const -> int { return title_logo_y_percent_; }
[[nodiscard]] auto titlePromptYPercent() const -> int { return title_prompt_y_percent_; }
[[nodiscard]] auto titleCopyrightYPercent() const -> int { return title_copyright_y_percent_; }
[[nodiscard]] auto titleMiniLogoGapPx() const -> int { return title_mini_logo_gap_px_; }
[[nodiscard]] auto titleBlinkVisibleMs() const -> int { return title_blink_visible_ms_; } [[nodiscard]] auto titleBlinkVisibleMs() const -> int { return title_blink_visible_ms_; }
[[nodiscard]] auto titleBlinkHiddenMs() const -> int { return title_blink_hidden_ms_; } [[nodiscard]] auto titleBlinkHiddenMs() const -> int { return title_blink_hidden_ms_; }
[[nodiscard]] auto titleStartingMs() const -> int { return title_starting_ms_; } [[nodiscard]] auto titleStartingMs() const -> int { return title_starting_ms_; }
@@ -101,9 +120,28 @@ class UiConfig {
[[nodiscard]] auto titleFadeOutMaxBlock() const -> int { return title_fade_out_max_block_; } [[nodiscard]] auto titleFadeOutMaxBlock() const -> int { return title_fade_out_max_block_; }
[[nodiscard]] auto titleMusicFadeOutMs() const -> int { return title_music_fade_out_ms_; } [[nodiscard]] auto titleMusicFadeOutMs() const -> int { return title_music_fade_out_ms_; }
[[nodiscard]] auto titleAttractFadeToBlackMs() const -> int { return title_attract_fade_to_black_ms_; } [[nodiscard]] auto titleAttractFadeToBlackMs() const -> int { return title_attract_fade_to_black_ms_; }
[[nodiscard]] auto titleIntroFadeInMs() const -> int { return title_intro_fade_in_ms_; } [[nodiscard]] auto titleIntro() const -> const TitleIntro& { return title_intro_; }
[[nodiscard]] auto titleHeatHaze() const -> const TitleHeatHaze& { return title_heat_haze_; } [[nodiscard]] auto titleHeatHaze() const -> const TitleHeatHaze& { return title_heat_haze_; }
// Escena LOGO — paràmetres del scanline reveal del logo de l'editora.
// Tot s'aplica només a la fase SCAN (substitueix l'antic FADE_IN).
struct LogoScan {
int duration_ms;
int band_height_px;
int band_alpha;
float band_pulse_hz;
float band_pulse_depth;
int glitch_amplitude_px;
int glitch_band_radius_px;
int vhs_burst_per_sec;
int vhs_burst_duration_ms;
int vhs_burst_height_px;
int vhs_burst_amplitude_px;
int flash_ms;
};
[[nodiscard]] auto logoScan() const -> const LogoScan& { return logo_scan_; }
// Corbes d'easing triades al YAML. Si el nom no és conegut, s'usa // Corbes d'easing triades al YAML. Si el nom no és conegut, s'usa
// Easing::linear com a fallback. // Easing::linear com a fallback.
[[nodiscard]] auto pauseEnterCurve() const -> CurveFn { return pause_enter_curve_; } [[nodiscard]] auto pauseEnterCurve() const -> CurveFn { return pause_enter_curve_; }
@@ -120,6 +158,7 @@ class UiConfig {
[[nodiscard]] auto menuSelectSound() const -> const std::string& { return menu_select_sound_; } [[nodiscard]] auto menuSelectSound() const -> const std::string& { return menu_select_sound_; }
[[nodiscard]] auto menuAcceptSound() const -> const std::string& { return menu_accept_sound_; } [[nodiscard]] auto menuAcceptSound() const -> const std::string& { return menu_accept_sound_; }
[[nodiscard]] auto playerStartSound() const -> const std::string& { return player_start_sound_; } [[nodiscard]] auto playerStartSound() const -> const std::string& { return player_start_sound_; }
[[nodiscard]] auto logoGlitchSound() const -> const std::string& { return logo_glitch_sound_; }
// Fonts de UI. // Fonts de UI.
[[nodiscard]] auto hudFont() const -> const Font& { return hud_font_; } [[nodiscard]] auto hudFont() const -> const Font& { return hud_font_; }
@@ -127,6 +166,7 @@ class UiConfig {
[[nodiscard]] auto roomTextFont() const -> const Font& { return room_text_font_; } [[nodiscard]] auto roomTextFont() const -> const Font& { return room_text_font_; }
[[nodiscard]] auto menuTitleFont() const -> const Font& { return menu_title_font_; } [[nodiscard]] auto menuTitleFont() const -> const Font& { return menu_title_font_; }
[[nodiscard]] auto menuItemsFont() const -> const Font& { return menu_items_font_; } [[nodiscard]] auto menuItemsFont() const -> const Font& { return menu_items_font_; }
[[nodiscard]] auto copyrightFont() const -> const Font& { return copyright_font_; }
// Geometria del Notifier. // Geometria del Notifier.
[[nodiscard]] auto notifier() const -> const Notifier& { return notifier_; } [[nodiscard]] auto notifier() const -> const Notifier& { return notifier_; }
@@ -173,6 +213,11 @@ class UiConfig {
Uint8 title_bg_color_; Uint8 title_bg_color_;
Uint8 title_text_color_; Uint8 title_text_color_;
Uint8 title_copyright_color_; Uint8 title_copyright_color_;
Uint8 title_press_start_outline_color_;
int title_logo_y_percent_;
int title_prompt_y_percent_;
int title_copyright_y_percent_;
int title_mini_logo_gap_px_;
int title_blink_visible_ms_; int title_blink_visible_ms_;
int title_blink_hidden_ms_; int title_blink_hidden_ms_;
int title_starting_ms_; int title_starting_ms_;
@@ -181,18 +226,21 @@ class UiConfig {
int title_fade_out_max_block_; int title_fade_out_max_block_;
int title_music_fade_out_ms_; int title_music_fade_out_ms_;
int title_attract_fade_to_black_ms_; int title_attract_fade_to_black_ms_;
int title_intro_fade_in_ms_; TitleIntro title_intro_;
TitleHeatHaze title_heat_haze_; TitleHeatHaze title_heat_haze_;
LogoScan logo_scan_;
std::string menu_select_sound_; std::string menu_select_sound_;
std::string menu_accept_sound_; std::string menu_accept_sound_;
std::string player_start_sound_; std::string player_start_sound_;
std::string logo_glitch_sound_;
Font hud_font_; Font hud_font_;
Font placeholder_font_; Font placeholder_font_;
Font room_text_font_; Font room_text_font_;
Font menu_title_font_; Font menu_title_font_;
Font menu_items_font_; Font menu_items_font_;
Font copyright_font_;
Notifier notifier_; Notifier notifier_;
+109 -28
View File
@@ -5,26 +5,23 @@
#include <algorithm> #include <algorithm>
#include <cstdint> #include <cstdint>
#include <iostream> #include <iostream>
#include <string>
#include <utility> #include <utility>
#include "core/audio/audio.hpp"
#include "core/input/global_inputs.hpp" #include "core/input/global_inputs.hpp"
#include "core/rendering/notifier.hpp" #include "core/rendering/notifier.hpp"
#include "core/rendering/render_info.hpp" #include "core/rendering/render_info.hpp"
#include "core/rendering/screen.hpp" #include "core/rendering/screen.hpp"
#include "core/rendering/text.hpp" #include "core/rendering/surface.hpp"
#include "core/system/event_buffer.hpp" #include "core/system/event_buffer.hpp"
#include "core/ui/ui_config.hpp" #include "core/ui/ui_config.hpp"
#include "game/options.hpp" #include "game/options.hpp"
#include "game/scenes/placeholder_input.hpp" #include "game/scenes/placeholder_input.hpp"
#include "game/scenes/scene_flow.hpp" #include "game/scenes/scene_flow.hpp"
#include "utils/defaults.hpp"
#include "utils/defines.hpp" #include "utils/defines.hpp"
namespace { namespace {
constexpr int NAME_Y_OFFSET = -8;
constexpr int TIMER_Y_OFFSET = 8;
// TODO: migrar a un logger unificat quan es decideixi (veure INC-03 a // TODO: migrar a un logger unificat quan es decideixi (veure INC-03 a
// docs/audit-core-rendering.md). De moment std::cout per rastrejar el // docs/audit-core-rendering.md). De moment std::cout per rastrejar el
// flux d'escenes als smoke runs. // flux d'escenes als smoke runs.
@@ -32,13 +29,64 @@ namespace {
} // namespace } // namespace
LogoScene::LogoScene(std::shared_ptr<Text> font) LogoScene::LogoScene(std::shared_ptr<Surface> logo, bool reset)
: font_(std::move(font)) { : logo_(std::move(logo)) {
std::cout << "[scene] " << SCENE_NAME << " start\n"; std::cout << "[scene] " << SCENE_NAME << (reset ? " start (reset)\n" : " start\n");
if (reset) {
if (auto* audio = Audio::get(); audio != nullptr) {
audio->stopMusic();
audio->stopAllSounds();
}
}
// So del scanline reveal: una sola pulsació al començament de la
// fase SCAN, en grup INTERFACE perquè no es vegi afectat pel volum
// de sons del joc i quedi sempre audible.
if (auto* audio = Audio::get(); audio != nullptr) {
audio->playSound(UiConfig::get().logoGlitchSound(), Audio::Group::INTERFACE);
}
// Arranca el revelat CRT: el primer frame ja pinta zero files (banda
// damunt del logo) i avança fins a tenir-lo visible al final.
const auto& s = UiConfig::get().logoScan();
scan_.begin({
.duration_ms = s.duration_ms,
.band_height_px = s.band_height_px,
.band_alpha = s.band_alpha,
.band_pulse_hz = s.band_pulse_hz,
.band_pulse_depth = s.band_pulse_depth,
.glitch_amplitude_px = s.glitch_amplitude_px,
.glitch_band_radius_px = s.glitch_band_radius_px,
.vhs_burst_per_sec = s.vhs_burst_per_sec,
.vhs_burst_duration_ms = s.vhs_burst_duration_ms,
.vhs_burst_height_px = s.vhs_burst_height_px,
.vhs_burst_amplitude_px = s.vhs_burst_amplitude_px,
.flash_ms = s.flash_ms,
});
} }
LogoScene::~LogoScene() = default; LogoScene::~LogoScene() = default;
auto LogoScene::wantsSkip() -> bool {
return std::ranges::any_of(EventBuffer::events, [](const SDL_Event& ev) {
return ev.type == SDL_EVENT_KEY_DOWN && !Placeholder::isReservedScancode(ev.key.scancode);
});
}
void LogoScene::enterMiddle() {
phase_ = Phase::MIDDLE;
phase_elapsed_ms_ = 0;
}
void LogoScene::enterFadeOut() {
phase_ = Phase::FADE_OUT;
phase_elapsed_ms_ = 0;
fade_.begin(FADE_OUT_MS, Transitions::FadeToBlack::Direction::FADE_OUT);
}
void LogoScene::enterDone() {
phase_ = Phase::DONE;
exit_request_ = ExitRequest{.next = SceneId::TITLE, .params = TitleParams{}};
}
void LogoScene::update(int delta_ms) { void LogoScene::update(int delta_ms) {
GlobalInputs::update(delta_ms); GlobalInputs::update(delta_ms);
if (GlobalInputs::wantsQuit()) { if (GlobalInputs::wantsQuit()) {
@@ -46,14 +94,44 @@ void LogoScene::update(int delta_ms) {
return; return;
} }
timer_ms_ -= delta_ms; // Skip: qualsevol tecla no reservada salta directament a la
// transició cap a TITLE. El propi TitleScene fa el seu fade-in
// inicial des de negre, així que la sortida no queda abrupta.
if (phase_ != Phase::DONE && wantsSkip()) {
enterDone();
}
const bool SKIPPED = std::ranges::any_of(EventBuffer::events, [](const SDL_Event& ev) { if (phase_ != Phase::DONE) {
return ev.type == SDL_EVENT_KEY_DOWN && !Placeholder::isReservedScancode(ev.key.scancode); phase_elapsed_ms_ += delta_ms;
}); // Tick dels efectes actius. `scan_` només fa feina visible
// durant Phase::SCAN; `fade_` només durant Phase::FADE_OUT. Tots
// dos són no-op si el seu duration_ms intern està a 0.
scan_.tick(delta_ms);
fade_.tick(delta_ms);
}
if (timer_ms_ <= 0 || SKIPPED) { switch (phase_) {
exit_request_ = ExitRequest{.next = SceneId::TITLE, .params = TitleParams{}}; case Phase::SCAN:
if (scan_.isFinished()) { enterMiddle(); }
break;
case Phase::MIDDLE:
// Música del títol al cor de la fase central. La crida és
// idempotent (Audio detecta same-track + same-loop i no
// reinicia), així que TitleScene podrà fer el seu propi
// playMusic sense interrompre la pista.
if (!music_started_ && phase_elapsed_ms_ >= MUSIC_TRIGGER_MS) {
music_started_ = true;
if (auto* audio = Audio::get(); audio != nullptr) {
audio->playMusic("title.ogg", -1);
}
}
if (phase_elapsed_ms_ >= MIDDLE_MS) { enterFadeOut(); }
break;
case Phase::FADE_OUT:
if (phase_elapsed_ms_ >= FADE_OUT_MS) { enterDone(); }
break;
case Phase::DONE:
break;
} }
Notifier::get().update(delta_ms); Notifier::get().update(delta_ms);
@@ -64,23 +142,26 @@ void LogoScene::render() {
constexpr std::uint32_t BLACK_ARGB = 0xFF000000U; constexpr std::uint32_t BLACK_ARGB = 0xFF000000U;
Screen::get().fillRect(0, 0, GameCanvas::WIDTH, GameCanvas::HEIGHT, BLACK_ARGB); Screen::get().fillRect(0, 0, GameCanvas::WIDTH, GameCanvas::HEIGHT, BLACK_ARGB);
if (font_) { if (logo_) {
const int SECS_LEFT = std::max(1, (timer_ms_ + 999) / 1000); const int X = (GameCanvas::WIDTH - logo_->getWidth()) / 2;
font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, const int Y = (GameCanvas::HEIGHT - logo_->getHeight()) / 2;
GameCanvas::CENTER_X, if (phase_ == Phase::SCAN) {
GameCanvas::CENTER_Y + NAME_Y_OFFSET, // Revelat CRT: la pròpia ScanReveal s'encarrega del blit
SCENE_NAME, // fila a fila, de la banda lluminosa, dels glitches i del
1, // flash final.
UiConfig::get().notificationText()); scan_.render(*logo_, X, Y);
font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, } else {
GameCanvas::CENTER_X, // MIDDLE i FADE_OUT: render normal, el logo ja està a
GameCanvas::CENTER_Y + TIMER_Y_OFFSET, // pantalla complet i estable.
std::to_string(SECS_LEFT), logo_->render(X, Y);
1, }
UiConfig::get().notificationText());
} }
Notifier::get().render(); Notifier::get().render();
// Capa de fosa: només FADE_OUT pinta negre semitransparent (el
// FADE_IN s'ha substituït per `scan_`). Va per damunt del logo i
// del Notifier, però per sota de RenderInfo (overlay de debug).
fade_.render();
RenderInfo::get().render(); RenderInfo::get().render();
Screen::get().present(); Screen::get().present();
} }
+46 -9
View File
@@ -1,26 +1,63 @@
#pragma once #pragma once
#include <cstdint>
#include <memory> #include <memory>
#include "core/rendering/effects/scan_reveal.hpp"
#include "core/rendering/transitions/fade_to_black.hpp"
#include "core/scene/scene.hpp" #include "core/scene/scene.hpp"
class Text; class Surface;
// Escena placeholder del logotip. Durada fixa (5 s) o skip amb qualsevol tecla // Escena del logotip de l'editora. FSM amb tres fases visibles:
// no reservada. Quan expira, publica un `ExitRequest` cap a TITLE; Director // `SCAN` — revelat tipus scanline CRT (durada YAML, ~1500 ms)
// és qui construeix la nova escena (l'escena no coneix els altres tipus i // `MIDDLE` — logo estable (`MIDDLE_MS`, 5 s)
// es pot exercir en tests sense arrossegar dependències). // `FADE_OUT` — fade lineal a negre (`FADE_OUT_MS`, 500 ms)
// I una terminal `DONE` que dispara la transició cap a TITLE. A 2,5 s
// dins de `MIDDLE` arrenca la música del títol perquè entri al cor de
// l'escena i continue cap a TitleScene sense talls (Audio ja és
// idempotent quan es demana la mateixa pista que ja sona).
//
// Skip: qualsevol tecla no reservada salta directament a la transició
// (sense fade-out) — el TitleScene té el seu propi `intro.fade_in_ms`,
// així que la transició queda visualment continua igualment.
//
// Si arriba amb `reset=true` (acció "Reiniciar" del menú de servei),
// atura música i tots els sons al constructor — el següent LOGO arrenca
// silenciós encara que veníssem del joc en marxa.
class LogoScene : public Scene { class LogoScene : public Scene {
public: public:
static constexpr int DURATION_MS = 5000; static constexpr int MIDDLE_MS = 5000;
static constexpr int FADE_OUT_MS = 500;
// Offset dins de MIDDLE en què comença a sonar la pista del títol;
// triat perquè quedi clarament al cor de la fase central (logo
// estable) i prou abans del fade-out per donar continuïtat sonora a
// TitleScene.
static constexpr int MUSIC_TRIGGER_MS = 2500;
explicit LogoScene(std::shared_ptr<Text> font); LogoScene(std::shared_ptr<Surface> logo, bool reset);
~LogoScene() override; ~LogoScene() override;
void update(int delta_ms) override; void update(int delta_ms) override;
void render() override; void render() override;
private: private:
std::shared_ptr<Text> font_; enum class Phase : std::uint8_t { SCAN,
int timer_ms_{DURATION_MS}; MIDDLE,
FADE_OUT,
DONE };
void enterMiddle();
void enterFadeOut();
void enterDone();
[[nodiscard]] static auto wantsSkip() -> bool;
std::shared_ptr<Surface> logo_;
Phase phase_{Phase::SCAN};
int phase_elapsed_ms_{0};
bool music_started_{false};
// Capa de fosa per al FADE_OUT cap a TITLE. El FADE_IN inicial s'ha
// substituït per `scan_` (revelat CRT amb glitch).
Transitions::FadeToBlack fade_;
Effects::ScanReveal scan_;
}; };
+9 -1
View File
@@ -30,7 +30,15 @@ enum class SceneId : std::uint8_t {
// rep cap dada de l'anterior; en cas contrari, només els camps que // rep cap dada de l'anterior; en cas contrari, només els camps que
// fluixen pel canal de transició (els recursos, defaults i config // fluixen pel canal de transició (els recursos, defaults i config
// interna els resol Director quan construeix). // interna els resol Director quan construeix).
struct LogoParams {}; struct LogoParams {
// Quan val `true`, indica que aquesta entrada al LOGO és una reinicialització
// completa (acció "Reiniciar" del menú de servei): l'escena haurà d'aturar
// música i sons abans de mostrar-se. En el flux normal LOGO→TITLE→… es
// queda a `false` perquè qualsevol música prèvia (p. ex. la pista de
// TITLE durant l'attract-out) continui sonant fins que la nova escena la
// substituïsca.
bool reset{false};
};
struct TitleParams {}; struct TitleParams {};
struct IntroParams { struct IntroParams {
DebugConfig::StartingPlayers starting_players{DebugConfig::StartingPlayers::BOTH}; DebugConfig::StartingPlayers starting_players{DebugConfig::StartingPlayers::BOTH};
+126 -73
View File
@@ -25,6 +25,7 @@
#include "game/scenes/scene_flow.hpp" #include "game/scenes/scene_flow.hpp"
#include "utils/defaults.hpp" #include "utils/defaults.hpp"
#include "utils/defines.hpp" #include "utils/defines.hpp"
#include "utils/easing.hpp"
namespace { namespace {
@@ -32,16 +33,6 @@ namespace {
// docs/audit-core-rendering.md). // docs/audit-core-rendering.md).
const char* const SCENE_NAME = "TITLE"; const char* const SCENE_NAME = "TITLE";
// Textos hardcodejats fins que tinguem artwork. Quan apareguen com
// a sprites, aquesta secció desapareix sencera.
const char* const TITLE_LINE_1 = "Aventures en Egipte";
const char* const TITLE_LINE_2 = "A R C A D E";
constexpr int TITLE_Y_PERCENT = 25;
constexpr int PROMPT_Y_PERCENT = 60;
constexpr int COPYRIGHT_Y_PERCENT = 90;
constexpr int LOGO_GAP_PX = 2; // Separació vertical entre el minilogo i el text de copyright.
// Detecta una pulsació de START dins el frame actual per al jugador // Detecta una pulsació de START dins el frame actual per al jugador
// indicat. Combina events de teclat (scancode hardcoded a Defaults:: // indicat. Combina events de teclat (scancode hardcoded a Defaults::
// KeysGame[P2]) i events de gamepad (botó remapejable via // KeysGame[P2]) i events de gamepad (botó remapejable via
@@ -63,14 +54,6 @@ namespace {
}); });
} }
// Separació vertical entre la primera i segona línia del títol. Una
// alçada de línia és l'opció més llegible al canvas de 240 px sense
// entrar a configuració extra; si demà cal afinar-ho, mou-se a
// ui.yaml.
[[nodiscard]] auto titleLineGap(const Text& font) -> int {
return font.getLineHeight();
}
// Reproducció del so de START. Centralitzat ací per disparar-lo des // Reproducció del so de START. Centralitzat ací per disparar-lo des
// dels punts on una pulsació té efecte real (entrada a STARTING o // dels punts on una pulsació té efecte real (entrada a STARTING o
// upgrade a BOTH); l'attract timeout no l'ha de tocar. // upgrade a BOTH); l'attract timeout no l'ha de tocar.
@@ -82,10 +65,11 @@ namespace {
} // namespace } // namespace
TitleScene::TitleScene(std::shared_ptr<Text> display_font, std::shared_ptr<Text> condensed_font, std::shared_ptr<Surface> background, std::shared_ptr<Surface> mini_logo) TitleScene::TitleScene(std::shared_ptr<Text> condensed_font, std::shared_ptr<Text> copyright_font, std::shared_ptr<Surface> background, std::shared_ptr<Surface> game_logo, std::shared_ptr<Surface> mini_logo)
: display_font_(std::move(display_font)), : condensed_font_(std::move(condensed_font)),
condensed_font_(std::move(condensed_font)), copyright_font_(std::move(copyright_font)),
background_(std::move(background)), background_(std::move(background)),
game_logo_(std::move(game_logo)),
mini_logo_(std::move(mini_logo)), mini_logo_(std::move(mini_logo)),
timer_ms_(UiConfig::get().titleDurationMs()) { timer_ms_(UiConfig::get().titleDurationMs()) {
std::cout << "[scene] " << SCENE_NAME << " start\n"; std::cout << "[scene] " << SCENE_NAME << " start\n";
@@ -104,7 +88,7 @@ TitleScene::TitleScene(std::shared_ptr<Text> display_font, std::shared_ptr<Text>
// Fade-in inicial: arrenquem en negre opac i ens esvaïm cap a la // Fade-in inicial: arrenquem en negre opac i ens esvaïm cap a la
// imatge de fons. Sense lligar a cap fase — viu sempre al frame i // imatge de fons. Sense lligar a cap fase — viu sempre al frame i
// un cop finished, `render()` és no-op fins que la TitleScene mor. // un cop finished, `render()` és no-op fins que la TitleScene mor.
intro_fade_.begin(UiConfig::get().titleIntroFadeInMs(), Transitions::FadeToBlack::Direction::FADE_IN); intro_fade_.begin(UiConfig::get().titleIntro().fade_in_ms, Transitions::FadeToBlack::Direction::FADE_IN);
// Música de fons en bucle. Es preloda al Director, així que la // Música de fons en bucle. Es preloda al Director, així que la
// primera crida és immediata. El fade-out arrenca en entrar a // primera crida és immediata. El fade-out arrenca en entrar a
@@ -138,6 +122,23 @@ void TitleScene::update(int delta_ms) {
// `starting_blink_period_ms`. // `starting_blink_period_ms`.
blink_elapsed_ms_ += delta_ms; blink_elapsed_ms_ += delta_ms;
// Rellotge de la seqüència d'intro. Avança sempre — `enterStarting()`
// ja el força al final de la seqüència, així el seu valor a STARTING
// i ATTRACT_OUT és inofensiu (els consumidors clampen).
intro_elapsed_ms_ += delta_ms;
if (!press_start_armed_) {
const auto& intro = UiConfig::get().titleIntro();
if (intro_elapsed_ms_ >= intro.press_start_at_ms) {
// Resetegem el cicle del parpadeig a l'instant en què el
// PRESS START passa a ser visible per primera vegada, perquè
// el primer frame caigui en estat "on" (si no, el modulus
// amb el contador acumulat des de t=0 podria caure mig
// cicle i donar un primer "off" estrany).
press_start_armed_ = true;
blink_elapsed_ms_ = 0;
}
}
const bool P1_START = pressedStart(0); const bool P1_START = pressedStart(0);
const bool P2_START = pressedStart(1); const bool P2_START = pressedStart(1);
@@ -165,6 +166,24 @@ void TitleScene::enterStarting(DebugConfig::StartingPlayers players) {
// STARTING comenci visible — sense això, el modulus amb el nou // STARTING comenci visible — sense això, el modulus amb el nou
// període podria caure mig cicle i donar un primer "off" estrany. // període podria caure mig cicle i donar un primer "off" estrany.
blink_elapsed_ms_ = 0; blink_elapsed_ms_ = 0;
// Shortcut: si l'usuari ha premut START durant la intro, saltem al
// final de la seqüència perquè el frame de STARTING comenci amb
// tot a la posició final i visible (sense salts ni elements en
// moviment a meitat). FadeToBlack::tick clampeja internament a
// duration_ms, així que un delta gegant l'avança a finished sense
// efectes secundaris.
const auto& intro = UiConfig::get().titleIntro();
const int INTRO_END_MS = std::max({
intro.game_logo_at_ms,
intro.mini_logo_at_ms + intro.mini_logo_slide_ms,
intro.copyright_at_ms + intro.copyright_slide_ms,
intro.press_start_at_ms,
});
intro_elapsed_ms_ = std::max(intro_elapsed_ms_, INTRO_END_MS);
press_start_armed_ = true;
intro_fade_.tick(INTRO_END_MS);
// Fade-out de la música. Si l'altre jugador prem START i el timer // Fade-out de la música. Si l'altre jugador prem START i el timer
// es reseteja, no rearranquem la música: es manté la fosa iniciada // es reseteja, no rearranquem la música: es manté la fosa iniciada
// ací (consistent amb el fade visual, que tampoc es reverteix). // ací (consistent amb el fade visual, que tampoc es reverteix).
@@ -254,9 +273,19 @@ void TitleScene::advanceToLogo() {
} }
void TitleScene::render() { void TitleScene::render() {
const auto& ui_config = UiConfig::get(); const auto& intro = UiConfig::get().titleIntro();
const auto& palette = Resource::Cache::get().getPalette("ui");
renderBackground();
renderGameLogo(intro);
renderPressStart(intro);
renderFooter(intro);
Notifier::get().render();
renderTransitions();
RenderInfo::get().render();
Screen::get().present();
}
void TitleScene::renderBackground() {
// Fons: la imatge si està disponible (cobreix tot el canvas amb la // Fons: la imatge si està disponible (cobreix tot el canvas amb la
// seua paleta nadiua), opcionalment amb heat-haze; en cas contrari, // seua paleta nadiua), opcionalment amb heat-haze; en cas contrari,
// color pla de la paleta UI com a fallback defensiu. // color pla de la paleta UI com a fallback defensiu.
@@ -266,40 +295,36 @@ void TitleScene::render() {
} else { } else {
background_->render(0, 0); background_->render(0, 0);
} }
} else { return;
}
const auto& ui_config = UiConfig::get();
const auto& palette = Resource::Cache::get().getPalette("ui");
Screen::get().fillRect(0, 0, GameCanvas::WIDTH, GameCanvas::HEIGHT, palette[ui_config.titleBgColor()]); Screen::get().fillRect(0, 0, GameCanvas::WIDTH, GameCanvas::HEIGHT, palette[ui_config.titleBgColor()]);
} }
const Uint8 TEXT_COLOR = ui_config.titleTextColor(); void TitleScene::renderGameLogo(const UiConfig::TitleIntro& intro) {
const Uint8 OUTLINE_COLOR = ui_config.titleCopyrightColor(); // Visibilitat binària gestionada per la seqüència d'intro: a IDLE
constexpr Uint8 OUTLINE_DIST = 1; // només es pinta a partir de `game_logo_at_ms`; fora d'IDLE sempre.
constexpr Uint8 STROKE4 = Text::CENTER_FLAG | Text::COLOR_FLAG | Text::STROKE4_FLAG; // TODO: aplicar fade-in alpha quan Surface suporti renderWithAlpha.
const bool VISIBLE = phase_ != Phase::IDLE || intro_elapsed_ms_ >= intro.game_logo_at_ms;
if (display_font_) { if (!game_logo_ || !VISIBLE) { return; }
const int TITLE_Y = (GameCanvas::HEIGHT * TITLE_Y_PERCENT) / 100; const auto& ui_config = UiConfig::get();
display_font_->writeDX(STROKE4, const int LOGO_Y = (GameCanvas::HEIGHT * ui_config.titleLogoYPercent()) / 100;
GameCanvas::CENTER_X, const int LOGO_X = GameCanvas::CENTER_X - (game_logo_->getWidth() / 2);
TITLE_Y, game_logo_->render(LOGO_X, LOGO_Y);
TITLE_LINE_1,
display_font_->getDefaultKerning(),
TEXT_COLOR,
OUTLINE_DIST,
OUTLINE_COLOR);
display_font_->writeDX(STROKE4,
GameCanvas::CENTER_X,
TITLE_Y + titleLineGap(*display_font_),
TITLE_LINE_2,
display_font_->getDefaultKerning(),
TEXT_COLOR,
OUTLINE_DIST,
OUTLINE_COLOR);
} }
if (condensed_font_) { void TitleScene::renderPressStart(const UiConfig::TitleIntro& intro) {
// A IDLE només apareix a partir de `press_start_at_ms`; fora d'IDLE
// (STARTING o ATTRACT_OUT) sempre, com fins ara.
const bool VISIBLE = phase_ != Phase::IDLE || intro_elapsed_ms_ >= intro.press_start_at_ms;
if (!condensed_font_ || !VISIBLE) { return; }
// Cicle de parpadeig: a IDLE pren visible_ms+hidden_ms del yaml // Cicle de parpadeig: a IDLE pren visible_ms+hidden_ms del yaml
// (asimètric, 80/20 per defecte); a STARTING commuta a 50/50 amb // (asimètric, 80/20 per defecte); a STARTING commuta a 50/50 amb
// `starting_blink_period_ms` per donar feedback de "polsat". // `starting_blink_period_ms` per donar feedback de "polsat".
// Si el cicle val 0 (configuració degenerada), cau a "sempre visible". // Si el cicle val 0 (configuració degenerada), cau a "sempre visible".
const auto& ui_config = UiConfig::get();
int cycle_ms = 0; int cycle_ms = 0;
int visible_ms = 0; int visible_ms = 0;
if (phase_ == Phase::STARTING) { if (phase_ == Phase::STARTING) {
@@ -309,44 +334,75 @@ void TitleScene::render() {
visible_ms = std::max(0, ui_config.titleBlinkVisibleMs()); visible_ms = std::max(0, ui_config.titleBlinkVisibleMs());
cycle_ms = visible_ms + std::max(0, ui_config.titleBlinkHiddenMs()); cycle_ms = visible_ms + std::max(0, ui_config.titleBlinkHiddenMs());
} }
const bool VISIBLE = (cycle_ms == 0) || ((blink_elapsed_ms_ % cycle_ms) < visible_ms); const bool BLINK_ON = (cycle_ms == 0) || ((blink_elapsed_ms_ % cycle_ms) < visible_ms);
if (!BLINK_ON) { return; }
if (VISIBLE) { constexpr Uint8 OUTLINE_DIST = 1;
const int PROMPT_Y = (GameCanvas::HEIGHT * PROMPT_Y_PERCENT) / 100; constexpr Uint8 STROKE4 = Text::CENTER_FLAG | Text::COLOR_FLAG | Text::STROKE4_FLAG;
const int PROMPT_Y = (GameCanvas::HEIGHT * ui_config.titlePromptYPercent()) / 100;
condensed_font_->writeDX(STROKE4, condensed_font_->writeDX(STROKE4,
GameCanvas::CENTER_X, GameCanvas::CENTER_X,
PROMPT_Y, PROMPT_Y,
Locale::get().text("title.press_start"), Locale::get().text("title.press_start"),
condensed_font_->getDefaultKerning() - 1, condensed_font_->getDefaultKerning() - 1,
TEXT_COLOR, ui_config.titleTextColor(),
OUTLINE_DIST, OUTLINE_DIST,
OUTLINE_COLOR); ui_config.titlePressStartOutlineColor());
}
} }
void TitleScene::renderFooter(const UiConfig::TitleIntro& intro) {
// Footer: minilogo centrat horitzontalment damunt del © de // Footer: minilogo centrat horitzontalment damunt del © de
// copyright. El text va al 90% Y, el logo just damunt amb un petit // copyright. El text va al `title.copyright_y_percent`, el logo just
// gap (LOGO_GAP_PX). Tots dos blocs són hardcodejats fins que // damunt amb un petit gap (`title.mini_logo_gap_px`). El copyright fa
// tinguem artwork final. // servir una font pixel-art pròpia (Tiny5-Regular size 8) — més neta
if (condensed_font_) { // i petita que la del "press start" perquè la jerarquia tipogràfica
const int COPY_Y = (GameCanvas::HEIGHT * COPYRIGHT_Y_PERCENT) / 100; // del peu quedi clara.
if (mini_logo_) { if (!copyright_font_) { return; }
const int LOGO_X = GameCanvas::CENTER_X - (mini_logo_->getWidth() / 2);
const int LOGO_Y = COPY_Y - mini_logo_->getHeight() - LOGO_GAP_PX; const auto& ui_config = UiConfig::get();
mini_logo_->render(LOGO_X, LOGO_Y); const int COPY_Y = (GameCanvas::HEIGHT * ui_config.titleCopyrightYPercent()) / 100;
const bool IDLE = phase_ == Phase::IDLE;
// Mini-logo: slide-in vertical des del marge inferior amb la corba
// `slide_curve`. Fora d'IDLE (o un cop completat) la Y és la final.
if (mini_logo_ && (!IDLE || intro_elapsed_ms_ >= intro.mini_logo_at_ms)) {
const int FINAL_Y = COPY_Y - mini_logo_->getHeight() - ui_config.titleMiniLogoGapPx();
int y = FINAL_Y;
if (IDLE && intro.mini_logo_slide_ms > 0) {
const float T = std::clamp(
static_cast<float>(intro_elapsed_ms_ - intro.mini_logo_at_ms) / static_cast<float>(intro.mini_logo_slide_ms),
0.0F,
1.0F);
y = Easing::lerpInt(GameCanvas::HEIGHT, FINAL_Y, intro.slide_curve(T));
} }
condensed_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, const int LOGO_X = GameCanvas::CENTER_X - (mini_logo_->getWidth() / 2);
mini_logo_->render(LOGO_X, y);
}
// Copyright: mateix patró amb el seu propi tempo. La diferència
// de temps entre `mini_logo_at_ms` i `copyright_at_ms` controla
// el decalatge entre les dues entrades (per defecte 200 ms).
if (!IDLE || intro_elapsed_ms_ >= intro.copyright_at_ms) {
int y = COPY_Y;
if (IDLE && intro.copyright_slide_ms > 0) {
const float T = std::clamp(
static_cast<float>(intro_elapsed_ms_ - intro.copyright_at_ms) / static_cast<float>(intro.copyright_slide_ms),
0.0F,
1.0F);
y = Easing::lerpInt(GameCanvas::HEIGHT, COPY_Y, intro.slide_curve(T));
}
copyright_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG,
GameCanvas::CENTER_X, GameCanvas::CENTER_X,
COPY_Y, y,
"© 2026 JailDesigner", "© 2026 JailDesigner",
condensed_font_->getDefaultKerning() - 1, copyright_font_->getDefaultKerning(),
ui_config.titleCopyrightColor()); ui_config.titleCopyrightColor());
} }
}
Notifier::get().render(); void TitleScene::renderTransitions() {
// Fade-in inicial: capa negra que s'esvaeix cap a l'escena durant // Fade-in inicial: capa negra que s'esvaeix cap a l'escena durant
// els primers `title.intro_fade_in_ms` ms. `render()` és no-op un // els primers `title.intro.fade_in_ms` ms. `render()` és no-op un
// cop finished, així que es queda penjat aquí sense lògica // cop finished, així que es queda penjat aquí sense lògica
// condicional. Va per damunt del Notifier — coherent amb la resta // condicional. Va per damunt del Notifier — coherent amb la resta
// de transicions — però per davall del RenderInfo de debug. // de transicions — però per davall del RenderInfo de debug.
@@ -362,7 +418,4 @@ void TitleScene::render() {
} else if (fade_out_ && (fade_out_->isActive() || fade_out_->isFinished())) { } else if (fade_out_ && (fade_out_->isActive() || fade_out_->isFinished())) {
fade_out_->render(); fade_out_->render();
} }
RenderInfo::get().render();
Screen::get().present();
} }
+21 -3
View File
@@ -8,6 +8,7 @@
#include "core/rendering/transitions/fade_to_black.hpp" #include "core/rendering/transitions/fade_to_black.hpp"
#include "core/rendering/transitions/mosaic.hpp" #include "core/rendering/transitions/mosaic.hpp"
#include "core/scene/scene.hpp" #include "core/scene/scene.hpp"
#include "core/ui/ui_config.hpp"
#include "game/debug_config.hpp" #include "game/debug_config.hpp"
class Surface; class Surface;
@@ -34,7 +35,7 @@ class TitleScene : public Scene {
STARTING, STARTING,
ATTRACT_OUT }; ATTRACT_OUT };
TitleScene(std::shared_ptr<Text> display_font, std::shared_ptr<Text> condensed_font, std::shared_ptr<Surface> background, std::shared_ptr<Surface> mini_logo); TitleScene(std::shared_ptr<Text> condensed_font, std::shared_ptr<Text> copyright_font, std::shared_ptr<Surface> background, std::shared_ptr<Surface> game_logo, std::shared_ptr<Surface> mini_logo);
~TitleScene() override; ~TitleScene() override;
void update(int delta_ms) override; void update(int delta_ms) override;
@@ -49,12 +50,29 @@ class TitleScene : public Scene {
void advanceToIntro(DebugConfig::StartingPlayers players); void advanceToIntro(DebugConfig::StartingPlayers players);
void advanceToLogo(); void advanceToLogo();
std::shared_ptr<Text> display_font_; // Helpers de `render()` extrets per evitar que la funció gran acumuli
// massa branques. Cadascun pinta un bloc independent del frame.
void renderBackground();
void renderGameLogo(const UiConfig::TitleIntro& intro);
void renderPressStart(const UiConfig::TitleIntro& intro);
void renderFooter(const UiConfig::TitleIntro& intro);
void renderTransitions();
std::shared_ptr<Text> condensed_font_; std::shared_ptr<Text> condensed_font_;
std::shared_ptr<Text> copyright_font_;
std::shared_ptr<Surface> background_; std::shared_ptr<Surface> background_;
std::shared_ptr<Surface> game_logo_;
std::shared_ptr<Surface> mini_logo_; std::shared_ptr<Surface> mini_logo_;
int timer_ms_; int timer_ms_;
int blink_elapsed_ms_{0}; int blink_elapsed_ms_{0};
// Rellotge de la seqüència d'intro (vegeu `title.intro` a ui.yaml).
// Avança sempre amb `delta_ms`. La visibilitat i el slide de cada
// element es deriven d'ell a `render()` — no cal cap altre estat.
int intro_elapsed_ms_{0};
// Detector de l'instant en què `intro_elapsed_ms_` creua per primera
// vegada `press_start_at_ms`: en aquell frame es reseteja
// `blink_elapsed_ms_` perquè el text aparegui en estat visible.
bool press_start_armed_{false};
std::optional<Effects::HeatHaze> heat_haze_; std::optional<Effects::HeatHaze> heat_haze_;
Phase phase_{Phase::IDLE}; Phase phase_{Phase::IDLE};
int starting_timer_ms_{0}; int starting_timer_ms_{0};
@@ -62,7 +80,7 @@ class TitleScene : public Scene {
std::optional<Transitions::Mosaic> fade_out_; std::optional<Transitions::Mosaic> fade_out_;
std::optional<Transitions::FadeToBlack> attract_fade_; std::optional<Transitions::FadeToBlack> attract_fade_;
// Fade-in inicial: capa negra que s'esvaeix cap a la imatge de fons // Fade-in inicial: capa negra que s'esvaeix cap a la imatge de fons
// durant els primers `title.intro_fade_in_ms` ms. Sempre activa al // durant els primers `title.intro.fade_in_ms` ms. Sempre activa al
// ctor; un cop finished, `render()` pinta alpha=0 (no-op) la resta // ctor; un cop finished, `render()` pinta alpha=0 (no-op) la resta
// del cicle de l'escena. // del cicle de l'escena.
Transitions::FadeToBlack intro_fade_; Transitions::FadeToBlack intro_fade_;
+9 -3
View File
@@ -1168,12 +1168,18 @@ auto ServiceMenu::buildSystem() -> Page {
page.items.push_back(makeLabel( page.items.push_back(makeLabel(
std::string("ver.") + Texts::VERSION + " (" + Version::GIT_HASH + ")")); std::string("ver.") + Texts::VERSION + " (" + Version::GIT_HASH + ")"));
// Reiniciar el joc: placeholder sense efecte fins que Director exposi // Reiniciar el joc: tornem al LOGO amb `reset=true` perquè LogoScene
// un pas de restart en calent (fora d'abast d'aquesta iteració). // ature música i sons abans de mostrar-se. Tanquem el menú a la
// mateixa acció — Director recollirà la transició al final del frame
// via `takePendingSceneRequest()` i reconstruirà l'escena.
page.items.push_back(makeAction( page.items.push_back(makeAction(
"menu.items.restart", "menu.items.restart",
[]() { []() {
// TODO: connectar amb un hipotètic Director::requestRestart(). ServiceMenu::get().requestSceneTransition(ExitRequest{
.next = SceneId::LOGO,
.params = LogoParams{.reset = true},
});
ServiceMenu::get().close();
})); }));
page.items.push_back(makeAction( page.items.push_back(makeAction(
+20
View File
@@ -6,11 +6,13 @@
#include <cstdint> #include <cstdint>
#include <functional> #include <functional>
#include <memory> #include <memory>
#include <optional>
#include <string> #include <string>
#include <vector> #include <vector>
#include "core/input/define_buttons.hpp" #include "core/input/define_buttons.hpp"
#include "core/rendering/surface.hpp" // Palette #include "core/rendering/surface.hpp" // Palette
#include "game/scenes/scene_flow.hpp"
class Text; class Text;
@@ -73,6 +75,19 @@ class ServiceMenu {
// `define_buttons_->enable(...)` de coffee_crisis_arcade_edition. // `define_buttons_->enable(...)` de coffee_crisis_arcade_edition.
void startDefineButtons(SDL_Gamepad* pad, const std::string& name, const std::string& path); void startDefineButtons(SDL_Gamepad* pad, const std::string& name, const std::string& path);
// Bústia de transició d'escena des del menú: alguns items (p.ex.
// "Reiniciar" a Sistema) no es poden expressar com a `ExitRequest` de
// l'escena actual perquè el menú és global i no té handle a l'escena.
// Director consulta `takePendingSceneRequest()` cada frame i, si hi ha
// valor, força la transició — el mateix camí que per a `ExitRequest`
// d'una escena, però el dispara el menú directament.
void requestSceneTransition(ExitRequest req) { pending_request_ = req; }
[[nodiscard]] auto takePendingSceneRequest() -> std::optional<ExitRequest> {
auto out = pending_request_;
pending_request_.reset();
return out;
}
private: private:
// --- Struct d'un item d'una pàgina --- // --- Struct d'un item d'una pàgina ---
// //
@@ -289,4 +304,9 @@ class ServiceMenu {
// persisteix la nova configuració via `GamepadBindings`. Replica el // persisteix la nova configuració via `GamepadBindings`. Replica el
// patró `define_buttons_` de coffee_crisis_arcade_edition. // patró `define_buttons_` de coffee_crisis_arcade_edition.
DefineButtons define_buttons_; DefineButtons define_buttons_;
// Sol·licitud de canvi d'escena pendent (vore `requestSceneTransition`).
// La consumeix Director al final del frame; si l'item no força la
// transició, queda buida.
std::optional<ExitRequest> pending_request_;
}; };
+89 -7
View File
@@ -156,6 +156,16 @@ namespace Defaults {
constexpr const char* MENU_ITEMS_GIF = "data/fonts/ari-w9500-condensed-display.gif"; constexpr const char* MENU_ITEMS_GIF = "data/fonts/ari-w9500-condensed-display.gif";
constexpr const char* MENU_ITEMS_FNT = "data/fonts/ari-w9500-condensed-display.fnt"; constexpr const char* MENU_ITEMS_FNT = "data/fonts/ari-w9500-condensed-display.fnt";
constexpr int MENU_ITEMS_KERNING = 0; constexpr int MENU_ITEMS_KERNING = 0;
// Copyright al peu de la TITLE: pixel-art estricte, generat amb
// font-gen a partir de Tiny5-Regular size 5 (caixa 3×6 px). La
// separem de la `placeholder` (que la fa servir el "press start")
// per diferenciar tipogràficament jerarquia: prompt destacat amb
// outline gros, copyright net i micro a baix.
constexpr const char* COPYRIGHT_NAME = "Tiny5-Regular";
constexpr const char* COPYRIGHT_GIF = "data/fonts/Tiny5-Regular.gif";
constexpr const char* COPYRIGHT_FNT = "data/fonts/Tiny5-Regular.fnt";
constexpr int COPYRIGHT_KERNING = 0;
} // namespace Font } // namespace Font
// Overlay de RenderInfo (F12). // Overlay de RenderInfo (F12).
@@ -251,6 +261,13 @@ namespace Defaults {
// `data/sounds/`. // `data/sounds/`.
constexpr const char* PLAYER_START_SOUND = "player_start.wav"; constexpr const char* PLAYER_START_SOUND = "player_start.wav";
// So curt de "soroll/scratch" disparat al constructor de LogoScene
// i sincronitzat amb la fase SCAN del scanline reveal. Playback
// únic via Audio::Group::INTERFACE; si dura més que el scan, la
// cua sona pinçada al començament de MIDDLE — pretès com a
// "eco" sonor de l'efecte.
constexpr const char* LOGO_GLITCH_SOUND = "glitch.wav";
// Escena TITLE (provisional fins que tinguem artwork). Colors // Escena TITLE (provisional fins que tinguem artwork). Colors
// referits a la paleta "ui" (uzebox.pal): BG_COLOR és el to més // referits a la paleta "ui" (uzebox.pal): BG_COLOR és el to més
// proper al negre sense ser-ho (índex 1 = 36,0,0); TEXT_COLOR és // proper al negre sense ser-ho (índex 1 = 36,0,0); TEXT_COLOR és
@@ -261,7 +278,22 @@ namespace Defaults {
constexpr int DURATION_MS = 30000; constexpr int DURATION_MS = 30000;
constexpr Uint8 BG_COLOR = 1; constexpr Uint8 BG_COLOR = 1;
constexpr Uint8 TEXT_COLOR = 31; constexpr Uint8 TEXT_COLOR = 31;
constexpr Uint8 COPYRIGHT_COLOR = 246; // (218,218,255) off-white amb un mínim de fred. // Color del text de "© 2026 JailDesigner": off-white càlid (índex
// 183 = 255,218,170, peach/cream) — ~90% blanc amb tint càlid.
constexpr Uint8 COPYRIGHT_COLOR = 183;
// Color de l'outline del prompt "PRESS START" (no del copyright,
// tot i compartir-se a versions antigues). Marró fosc càlid
// (índex 19 = 109,72,0) coherent amb la paleta del logo.
constexpr Uint8 PRESS_START_OUTLINE_COLOR = 19;
// Posicions Y dels tres elements verticals de la TITLE, en
// percentatge sobre `GameCanvas::HEIGHT`. El logo s'ancora pel
// seu top (la imatge cau cap avall des d'aquest Y); el prompt
// i el copyright es centren verticalment sobre la línia.
constexpr int LOGO_Y_PERCENT = 15;
constexpr int PROMPT_Y_PERCENT = 68;
constexpr int COPYRIGHT_Y_PERCENT = 92;
// Separació vertical entre el minilogo i el text del © al peu.
constexpr int MINI_LOGO_GAP_PX = 2;
constexpr int BLINK_VISIBLE_MS = 800; constexpr int BLINK_VISIBLE_MS = 800;
constexpr int BLINK_HIDDEN_MS = 200; constexpr int BLINK_HIDDEN_MS = 200;
@@ -294,12 +326,25 @@ namespace Defaults {
// veurà què en fa. // veurà què en fa.
constexpr int ATTRACT_FADE_TO_BLACK_MS = 800; constexpr int ATTRACT_FADE_TO_BLACK_MS = 800;
// Fade-in inicial de l'escena: capa negra al començament que // Seqüència d'aparició dels elements de la TITLE en fase IDLE.
// s'esvaeix cap a la imatge de fons. Lineal, sense mosaic — // Tots els temps són absoluts des de l'entrada a l'escena.
// mateixa convenció que el `room_intro.fade_in_ms` del joc // `FADE_IN_MS` és la capa negra inicial que s'esvaeix cap al fons
// perquè l'arribada a la TITLE i l'arribada a una sala de // (mateixa convenció que `room_intro.fade_in_ms` del joc perquè
// joc tinguin el mateix ritme visual. // l'arribada a la TITLE i a una sala de joc tinguin el mateix
constexpr int INTRO_FADE_IN_MS = 500; // ritme). Els altres temps marquen quan apareix cada element;
// `SLIDE_*` defineixen el slide-in des del marge inferior amb
// la corba `SLIDE_CURVE`. Si l'usuari prem START durant la
// seqüència, aquesta es completa instantàniament.
namespace Intro {
constexpr int FADE_IN_MS = 500;
constexpr int GAME_LOGO_AT_MS = 1500;
constexpr int MINI_LOGO_AT_MS = 2000;
constexpr int MINI_LOGO_SLIDE_MS = 500;
constexpr int COPYRIGHT_AT_MS = 2200;
constexpr int COPYRIGHT_SLIDE_MS = 500;
constexpr const char* SLIDE_CURVE = "outCubic";
constexpr int PRESS_START_AT_MS = 3000;
} // namespace Intro
// Distorsió sinusoidal horitzontal de l'imatge de fons // Distorsió sinusoidal horitzontal de l'imatge de fons
// (efecte de calor del desert). `amplitude_px` és el // (efecte de calor del desert). `amplitude_px` és el
@@ -314,6 +359,43 @@ namespace Defaults {
constexpr float SPEED_HZ = 0.6F; constexpr float SPEED_HZ = 0.6F;
} // namespace HeatHaze } // namespace HeatHaze
} // namespace Title } // namespace Title
// Escena LOGO (logotip de l'editora). El revelat del logo a la
// fase SCAN imita un escaneig CRT amb una banda lluminosa que
// baixa de dalt a baix, glitch al voltant de la banda i bursts
// VHS ocasionals damunt la part ja revelada. Quan la banda toca
// el peu del logo, un flash blanc breu remata l'efecte i s'entra
// a MIDDLE (logo estable).
namespace Logo::Scan {
constexpr int DURATION_MS = 1500;
constexpr int BAND_HEIGHT_PX = 4;
constexpr int BAND_ALPHA = 200;
// Pulse de brillantor sinusoidal sobre l'alpha base de la
// banda — simula el ritme de refresc d'un CRT. La fase
// avança a `BAND_PULSE_HZ` cicles/segon i la profunditat
// marca quant cau l'alpha al ventre (0 = sense pulse,
// 1 = la banda arriba a alpha 0 als ventres). Valors per
// defecte: 20 Hz pulsacional ben visible a 60 fps sense
// aliasing dur, profunditat 0.45 (alpha oscil·la entre el
// 55% i el 100% de BAND_ALPHA).
constexpr float BAND_PULSE_HZ = 20.0F;
constexpr float BAND_PULSE_DEPTH = 0.45F;
// Glitch a la banda d'impacte: les files dins del radi
// reben un offset horitzontal aleatori d'amplitud
// decreixent amb la distància a la banda.
constexpr int GLITCH_AMPLITUDE_PX = 6;
constexpr int GLITCH_BAND_RADIUS_PX = 12;
// Bursts VHS damunt la part ja revelada: ocasionalment
// (~per_sec mitjà) una franja horitzontal es desplaça
// durant `duration_ms` per simular tracking error.
constexpr int VHS_BURST_PER_SEC = 3;
constexpr int VHS_BURST_DURATION_MS = 80;
constexpr int VHS_BURST_HEIGHT_PX = 8;
constexpr int VHS_BURST_AMPLITUDE_PX = 8;
// Flash blanc final quan la banda surt per sota — punt
// visual de tancament del scan abans d'entrar a MIDDLE.
constexpr int FLASH_MS = 100;
} // namespace Logo::Scan
} // namespace UI } // namespace UI
#ifdef _DEBUG #ifdef _DEBUG
+1 -1
View File
@@ -3,7 +3,7 @@
namespace Texts { namespace Texts {
constexpr const char* WINDOW_TITLE = "© 2026 Aventures en Egipte ARCADE — JailDesigner"; constexpr const char* WINDOW_TITLE = "© 2026 Aventures en Egipte ARCADE — JailDesigner";
constexpr const char* COPYRIGHT = "© 2026 JailDesigner"; constexpr const char* COPYRIGHT = "© 2026 JailDesigner";
constexpr const char* VERSION = "0.34.0"; // Single source of truth — Makefile el grep constexpr const char* VERSION = "0.35.0"; // Single source of truth — Makefile el grep
} // namespace Texts } // namespace Texts
namespace GameCanvas { namespace GameCanvas {