Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d5a4cee8e | ||
|
|
d560623e00 | ||
|
|
3f88dfaaee | ||
|
|
e1bd75e00d | ||
|
|
e32cff9dac | ||
|
|
243ced7498 | ||
|
|
fceeb11cdd | ||
|
|
233fcb8226 | ||
|
|
8bc45ccedc | ||
|
|
f7ab6a641a |
No files matched your search
@@ -93,6 +93,7 @@ set(APP_SOURCES
|
||||
source/core/rendering/sprite/animated_sprite.cpp
|
||||
source/core/rendering/sprite/animated_sprite_parser.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/fade_to_black.cpp
|
||||
source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp
|
||||
|
||||
@@ -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
@@ -58,6 +58,15 @@ fonts:
|
||||
gif: "data/fonts/ari-w9500-condensed-display.gif"
|
||||
fnt: "data/fonts/ari-w9500-condensed-display.fnt"
|
||||
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:
|
||||
bg_enabled: false
|
||||
@@ -101,6 +110,9 @@ sounds:
|
||||
# Disparat quan un jugador prem START a la TITLE (primera pulsació o
|
||||
# la del segon jugador dins de la finestra de STARTING).
|
||||
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).
|
||||
# `bg_color` i `text_color` són índexs sobre la paleta "ui" (uzebox.pal):
|
||||
@@ -113,7 +125,23 @@ title:
|
||||
duration_ms: 30000
|
||||
bg_color: 1
|
||||
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_hidden_ms: 200
|
||||
# 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:
|
||||
# `title.ogg` continua sonant cap a LOGO, que ja veurà què en fa.
|
||||
attract_fade_to_black_ms: 800
|
||||
# Fade-in inicial: capa negra al començament que s'esvaeix cap al fons.
|
||||
# Mateix valor que `room_intro.fade_in_ms` (data/scenes/game.yaml) perquè
|
||||
# l'arribada a la TITLE i a una sala de joc tinguin el mateix ritme.
|
||||
intro_fade_in_ms: 500
|
||||
# Seqüència d'aparició dels elements de la TITLE en fase IDLE. Tots
|
||||
# els temps són absoluts des de l'entrada a l'escena. `fade_in_ms` és
|
||||
# la capa negra inicial que s'esvaeix cap al fons; els altres marquen
|
||||
# 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).
|
||||
# `amplitude_px`: desplaçament màxim de cada fila en píxels.
|
||||
# `wavelength_px`: distància vertical entre dos pics de l'ona.
|
||||
@@ -153,3 +191,27 @@ title:
|
||||
amplitude_px: 1.5
|
||||
wavelength_px: 32.0
|
||||
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
|
||||
@@ -170,6 +170,10 @@ Director::Director() {
|
||||
// prem START (vore enterStarting i el reset del segon jugador).
|
||||
const auto& player_start = UiConfig::get().playerStartSound();
|
||||
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
|
||||
// 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_ = 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`
|
||||
// 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.
|
||||
@@ -213,7 +227,6 @@ Director::Director() {
|
||||
};
|
||||
auto menu_title_text = load_menu_font(UiConfig::get().menuTitleFont());
|
||||
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
|
||||
// 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);
|
||||
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
|
||||
// la seua paleta nadiua perquè el GIF ja porta els colors finals.
|
||||
constexpr const char* TITLE_BG_KEY = "title_bg.gif";
|
||||
@@ -393,6 +420,16 @@ void Director::processMenuInput() {
|
||||
|
||||
void Director::switchSceneIfRequested() {
|
||||
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_`.
|
||||
// Director el desempaqueta i construeix la nova escena via
|
||||
// `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
|
||||
// migrades a `ExitRequest`, aquestes lambdes desapareixeran.
|
||||
switch (id) {
|
||||
case SceneId::LOGO:
|
||||
return std::make_unique<LogoScene>(placeholder_text_);
|
||||
case SceneId::LOGO: {
|
||||
const auto& p = std::get<LogoParams>(params);
|
||||
return std::make_unique<LogoScene>(logo_full_surface_, p.reset);
|
||||
}
|
||||
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: {
|
||||
const auto& p = std::get<IntroParams>(params);
|
||||
return std::make_unique<IntroScene>(placeholder_text_, p.starting_players);
|
||||
|
||||
@@ -30,9 +30,11 @@ class Director {
|
||||
// escena. No són `static`/Meyers perquè depenen de l'inici d'SDL i
|
||||
// de Resource::Cache.
|
||||
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> game_logo_surface_;
|
||||
std::shared_ptr<Surface> mini_logo_surface_;
|
||||
std::shared_ptr<Surface> logo_full_surface_;
|
||||
|
||||
void createSystemFolder();
|
||||
void processEvents();
|
||||
|
||||
@@ -100,6 +100,11 @@ UiConfig::UiConfig()
|
||||
title_bg_color_(Defaults::UI::Title::BG_COLOR),
|
||||
title_text_color_(Defaults::UI::Title::TEXT_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_hidden_ms_(Defaults::UI::Title::BLINK_HIDDEN_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_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_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_{
|
||||
.enabled = Defaults::UI::Title::HeatHaze::ENABLED,
|
||||
.amplitude_px = Defaults::UI::Title::HeatHaze::AMPLITUDE_PX,
|
||||
.wavelength_px = Defaults::UI::Title::HeatHaze::WAVELENGTH_PX,
|
||||
.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_accept_sound_(Defaults::UI::MENU_ACCEPT_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},
|
||||
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},
|
||||
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},
|
||||
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_{
|
||||
.padding_h = Defaults::UI::Notifier::PADDING_H,
|
||||
.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, "menu_title", menu_title_font_);
|
||||
readFont(f, "menu_items", menu_items_font_);
|
||||
readFont(f, "copyright", copyright_font_);
|
||||
}
|
||||
|
||||
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_accept", menu_accept_sound_);
|
||||
readField(s, "player_start", player_start_sound_);
|
||||
readField(s, "logo_glitch", logo_glitch_sound_);
|
||||
}
|
||||
|
||||
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, "text_color", title_text_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_hidden_ms", title_blink_hidden_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, "music_fade_out_ms", title_music_fade_out_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")) {
|
||||
const auto& hh = t["heat_haze"];
|
||||
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")) {
|
||||
const auto& n = yaml["notifier"];
|
||||
readField(n, "padding_h", notifier_.padding_h);
|
||||
|
||||
@@ -89,10 +89,29 @@ class UiConfig {
|
||||
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 titleBgColor() const -> Uint8 { return title_bg_color_; }
|
||||
[[nodiscard]] auto titleTextColor() const -> Uint8 { return title_text_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 titleBlinkHiddenMs() const -> int { return title_blink_hidden_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 titleMusicFadeOutMs() const -> int { return title_music_fade_out_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_; }
|
||||
|
||||
// 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
|
||||
// Easing::linear com a fallback.
|
||||
[[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 menuAcceptSound() const -> const std::string& { return menu_accept_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.
|
||||
[[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 menuTitleFont() const -> const Font& { return menu_title_font_; }
|
||||
[[nodiscard]] auto menuItemsFont() const -> const Font& { return menu_items_font_; }
|
||||
[[nodiscard]] auto copyrightFont() const -> const Font& { return copyright_font_; }
|
||||
|
||||
// Geometria del Notifier.
|
||||
[[nodiscard]] auto notifier() const -> const Notifier& { return notifier_; }
|
||||
@@ -173,6 +213,11 @@ class UiConfig {
|
||||
Uint8 title_bg_color_;
|
||||
Uint8 title_text_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_hidden_ms_;
|
||||
int title_starting_ms_;
|
||||
@@ -181,18 +226,21 @@ class UiConfig {
|
||||
int title_fade_out_max_block_;
|
||||
int title_music_fade_out_ms_;
|
||||
int title_attract_fade_to_black_ms_;
|
||||
int title_intro_fade_in_ms_;
|
||||
TitleIntro title_intro_;
|
||||
TitleHeatHaze title_heat_haze_;
|
||||
LogoScan logo_scan_;
|
||||
|
||||
std::string menu_select_sound_;
|
||||
std::string menu_accept_sound_;
|
||||
std::string player_start_sound_;
|
||||
std::string logo_glitch_sound_;
|
||||
|
||||
Font hud_font_;
|
||||
Font placeholder_font_;
|
||||
Font room_text_font_;
|
||||
Font menu_title_font_;
|
||||
Font menu_items_font_;
|
||||
Font copyright_font_;
|
||||
|
||||
Notifier notifier_;
|
||||
|
||||
|
||||
@@ -5,26 +5,23 @@
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include "core/audio/audio.hpp"
|
||||
#include "core/input/global_inputs.hpp"
|
||||
#include "core/rendering/notifier.hpp"
|
||||
#include "core/rendering/render_info.hpp"
|
||||
#include "core/rendering/screen.hpp"
|
||||
#include "core/rendering/text.hpp"
|
||||
#include "core/rendering/surface.hpp"
|
||||
#include "core/system/event_buffer.hpp"
|
||||
#include "core/ui/ui_config.hpp"
|
||||
#include "game/options.hpp"
|
||||
#include "game/scenes/placeholder_input.hpp"
|
||||
#include "game/scenes/scene_flow.hpp"
|
||||
#include "utils/defaults.hpp"
|
||||
#include "utils/defines.hpp"
|
||||
|
||||
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
|
||||
// docs/audit-core-rendering.md). De moment std::cout per rastrejar el
|
||||
// flux d'escenes als smoke runs.
|
||||
@@ -32,13 +29,64 @@ namespace {
|
||||
|
||||
} // namespace
|
||||
|
||||
LogoScene::LogoScene(std::shared_ptr<Text> font)
|
||||
: font_(std::move(font)) {
|
||||
std::cout << "[scene] " << SCENE_NAME << " start\n";
|
||||
LogoScene::LogoScene(std::shared_ptr<Surface> logo, bool reset)
|
||||
: logo_(std::move(logo)) {
|
||||
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;
|
||||
|
||||
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) {
|
||||
GlobalInputs::update(delta_ms);
|
||||
if (GlobalInputs::wantsQuit()) {
|
||||
@@ -46,14 +94,44 @@ void LogoScene::update(int delta_ms) {
|
||||
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) {
|
||||
return ev.type == SDL_EVENT_KEY_DOWN && !Placeholder::isReservedScancode(ev.key.scancode);
|
||||
});
|
||||
if (phase_ != Phase::DONE) {
|
||||
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) {
|
||||
exit_request_ = ExitRequest{.next = SceneId::TITLE, .params = TitleParams{}};
|
||||
switch (phase_) {
|
||||
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);
|
||||
@@ -64,23 +142,26 @@ void LogoScene::render() {
|
||||
constexpr std::uint32_t BLACK_ARGB = 0xFF000000U;
|
||||
Screen::get().fillRect(0, 0, GameCanvas::WIDTH, GameCanvas::HEIGHT, BLACK_ARGB);
|
||||
|
||||
if (font_) {
|
||||
const int SECS_LEFT = std::max(1, (timer_ms_ + 999) / 1000);
|
||||
font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG,
|
||||
GameCanvas::CENTER_X,
|
||||
GameCanvas::CENTER_Y + NAME_Y_OFFSET,
|
||||
SCENE_NAME,
|
||||
1,
|
||||
UiConfig::get().notificationText());
|
||||
font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG,
|
||||
GameCanvas::CENTER_X,
|
||||
GameCanvas::CENTER_Y + TIMER_Y_OFFSET,
|
||||
std::to_string(SECS_LEFT),
|
||||
1,
|
||||
UiConfig::get().notificationText());
|
||||
if (logo_) {
|
||||
const int X = (GameCanvas::WIDTH - logo_->getWidth()) / 2;
|
||||
const int Y = (GameCanvas::HEIGHT - logo_->getHeight()) / 2;
|
||||
if (phase_ == Phase::SCAN) {
|
||||
// Revelat CRT: la pròpia ScanReveal s'encarrega del blit
|
||||
// fila a fila, de la banda lluminosa, dels glitches i del
|
||||
// flash final.
|
||||
scan_.render(*logo_, X, Y);
|
||||
} else {
|
||||
// MIDDLE i FADE_OUT: render normal, el logo ja està a
|
||||
// pantalla complet i estable.
|
||||
logo_->render(X, Y);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
Screen::get().present();
|
||||
}
|
||||
@@ -1,26 +1,63 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
#include "core/rendering/effects/scan_reveal.hpp"
|
||||
#include "core/rendering/transitions/fade_to_black.hpp"
|
||||
#include "core/scene/scene.hpp"
|
||||
|
||||
class Text;
|
||||
class Surface;
|
||||
|
||||
// Escena placeholder del logotip. Durada fixa (5 s) o skip amb qualsevol tecla
|
||||
// no reservada. Quan expira, publica un `ExitRequest` cap a TITLE; Director
|
||||
// és qui construeix la nova escena (l'escena no coneix els altres tipus i
|
||||
// es pot exercir en tests sense arrossegar dependències).
|
||||
// Escena del logotip de l'editora. FSM amb tres fases visibles:
|
||||
// `SCAN` — revelat tipus scanline CRT (durada YAML, ~1500 ms)
|
||||
// `MIDDLE` — logo estable (`MIDDLE_MS`, 5 s)
|
||||
// `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 {
|
||||
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;
|
||||
|
||||
void update(int delta_ms) override;
|
||||
void render() override;
|
||||
|
||||
private:
|
||||
std::shared_ptr<Text> font_;
|
||||
int timer_ms_{DURATION_MS};
|
||||
enum class Phase : std::uint8_t { SCAN,
|
||||
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_;
|
||||
};
|
||||
@@ -30,7 +30,15 @@ enum class SceneId : std::uint8_t {
|
||||
// rep cap dada de l'anterior; en cas contrari, només els camps que
|
||||
// fluixen pel canal de transició (els recursos, defaults i config
|
||||
// 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 IntroParams {
|
||||
DebugConfig::StartingPlayers starting_players{DebugConfig::StartingPlayers::BOTH};
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include "game/scenes/scene_flow.hpp"
|
||||
#include "utils/defaults.hpp"
|
||||
#include "utils/defines.hpp"
|
||||
#include "utils/easing.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -32,16 +33,6 @@ namespace {
|
||||
// docs/audit-core-rendering.md).
|
||||
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
|
||||
// indicat. Combina events de teclat (scancode hardcoded a Defaults::
|
||||
// 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
|
||||
// dels punts on una pulsació té efecte real (entrada a STARTING o
|
||||
// upgrade a BOTH); l'attract timeout no l'ha de tocar.
|
||||
@@ -82,10 +65,11 @@ 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)
|
||||
: display_font_(std::move(display_font)),
|
||||
condensed_font_(std::move(condensed_font)),
|
||||
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)
|
||||
: condensed_font_(std::move(condensed_font)),
|
||||
copyright_font_(std::move(copyright_font)),
|
||||
background_(std::move(background)),
|
||||
game_logo_(std::move(game_logo)),
|
||||
mini_logo_(std::move(mini_logo)),
|
||||
timer_ms_(UiConfig::get().titleDurationMs()) {
|
||||
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
|
||||
// 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.
|
||||
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
|
||||
// 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`.
|
||||
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 P2_START = pressedStart(1);
|
||||
|
||||
@@ -165,6 +166,24 @@ void TitleScene::enterStarting(DebugConfig::StartingPlayers players) {
|
||||
// STARTING comenci visible — sense això, el modulus amb el nou
|
||||
// període podria caure mig cicle i donar un primer "off" estrany.
|
||||
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
|
||||
// es reseteja, no rearranquem la música: es manté la fosa iniciada
|
||||
// ací (consistent amb el fade visual, que tampoc es reverteix).
|
||||
@@ -254,9 +273,19 @@ void TitleScene::advanceToLogo() {
|
||||
}
|
||||
|
||||
void TitleScene::render() {
|
||||
const auto& ui_config = UiConfig::get();
|
||||
const auto& palette = Resource::Cache::get().getPalette("ui");
|
||||
const auto& intro = UiConfig::get().titleIntro();
|
||||
|
||||
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
|
||||
// seua paleta nadiua), opcionalment amb heat-haze; en cas contrari,
|
||||
// color pla de la paleta UI com a fallback defensiu.
|
||||
@@ -266,87 +295,114 @@ void TitleScene::render() {
|
||||
} else {
|
||||
background_->render(0, 0);
|
||||
}
|
||||
} else {
|
||||
Screen::get().fillRect(0, 0, GameCanvas::WIDTH, GameCanvas::HEIGHT, palette[ui_config.titleBgColor()]);
|
||||
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()]);
|
||||
}
|
||||
|
||||
void TitleScene::renderGameLogo(const UiConfig::TitleIntro& intro) {
|
||||
// Visibilitat binària gestionada per la seqüència d'intro: a IDLE
|
||||
// només es pinta a partir de `game_logo_at_ms`; fora d'IDLE sempre.
|
||||
// TODO: aplicar fade-in alpha quan Surface suporti renderWithAlpha.
|
||||
const bool VISIBLE = phase_ != Phase::IDLE || intro_elapsed_ms_ >= intro.game_logo_at_ms;
|
||||
if (!game_logo_ || !VISIBLE) { return; }
|
||||
const auto& ui_config = UiConfig::get();
|
||||
const int LOGO_Y = (GameCanvas::HEIGHT * ui_config.titleLogoYPercent()) / 100;
|
||||
const int LOGO_X = GameCanvas::CENTER_X - (game_logo_->getWidth() / 2);
|
||||
game_logo_->render(LOGO_X, LOGO_Y);
|
||||
}
|
||||
|
||||
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
|
||||
// (asimètric, 80/20 per defecte); a STARTING commuta a 50/50 amb
|
||||
// `starting_blink_period_ms` per donar feedback de "polsat".
|
||||
// Si el cicle val 0 (configuració degenerada), cau a "sempre visible".
|
||||
const auto& ui_config = UiConfig::get();
|
||||
int cycle_ms = 0;
|
||||
int visible_ms = 0;
|
||||
if (phase_ == Phase::STARTING) {
|
||||
cycle_ms = std::max(0, ui_config.titleStartingBlinkPeriodMs());
|
||||
visible_ms = cycle_ms / 2;
|
||||
} else {
|
||||
visible_ms = std::max(0, ui_config.titleBlinkVisibleMs());
|
||||
cycle_ms = visible_ms + std::max(0, ui_config.titleBlinkHiddenMs());
|
||||
}
|
||||
const bool BLINK_ON = (cycle_ms == 0) || ((blink_elapsed_ms_ % cycle_ms) < visible_ms);
|
||||
if (!BLINK_ON) { return; }
|
||||
|
||||
const Uint8 TEXT_COLOR = ui_config.titleTextColor();
|
||||
const Uint8 OUTLINE_COLOR = ui_config.titleCopyrightColor();
|
||||
constexpr Uint8 OUTLINE_DIST = 1;
|
||||
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,
|
||||
GameCanvas::CENTER_X,
|
||||
PROMPT_Y,
|
||||
Locale::get().text("title.press_start"),
|
||||
condensed_font_->getDefaultKerning() - 1,
|
||||
ui_config.titleTextColor(),
|
||||
OUTLINE_DIST,
|
||||
ui_config.titlePressStartOutlineColor());
|
||||
}
|
||||
|
||||
if (display_font_) {
|
||||
const int TITLE_Y = (GameCanvas::HEIGHT * TITLE_Y_PERCENT) / 100;
|
||||
display_font_->writeDX(STROKE4,
|
||||
GameCanvas::CENTER_X,
|
||||
TITLE_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_) {
|
||||
// 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
|
||||
// `starting_blink_period_ms` per donar feedback de "polsat".
|
||||
// Si el cicle val 0 (configuració degenerada), cau a "sempre visible".
|
||||
int cycle_ms = 0;
|
||||
int visible_ms = 0;
|
||||
if (phase_ == Phase::STARTING) {
|
||||
cycle_ms = std::max(0, ui_config.titleStartingBlinkPeriodMs());
|
||||
visible_ms = cycle_ms / 2;
|
||||
} else {
|
||||
visible_ms = std::max(0, ui_config.titleBlinkVisibleMs());
|
||||
cycle_ms = visible_ms + std::max(0, ui_config.titleBlinkHiddenMs());
|
||||
}
|
||||
const bool VISIBLE = (cycle_ms == 0) || ((blink_elapsed_ms_ % cycle_ms) < visible_ms);
|
||||
|
||||
if (VISIBLE) {
|
||||
const int PROMPT_Y = (GameCanvas::HEIGHT * PROMPT_Y_PERCENT) / 100;
|
||||
condensed_font_->writeDX(STROKE4,
|
||||
GameCanvas::CENTER_X,
|
||||
PROMPT_Y,
|
||||
Locale::get().text("title.press_start"),
|
||||
condensed_font_->getDefaultKerning() - 1,
|
||||
TEXT_COLOR,
|
||||
OUTLINE_DIST,
|
||||
OUTLINE_COLOR);
|
||||
}
|
||||
}
|
||||
|
||||
void TitleScene::renderFooter(const UiConfig::TitleIntro& intro) {
|
||||
// Footer: minilogo centrat horitzontalment damunt del © de
|
||||
// copyright. El text va al 90% Y, el logo just damunt amb un petit
|
||||
// gap (LOGO_GAP_PX). Tots dos blocs són hardcodejats fins que
|
||||
// tinguem artwork final.
|
||||
if (condensed_font_) {
|
||||
const int COPY_Y = (GameCanvas::HEIGHT * COPYRIGHT_Y_PERCENT) / 100;
|
||||
if (mini_logo_) {
|
||||
const int LOGO_X = GameCanvas::CENTER_X - (mini_logo_->getWidth() / 2);
|
||||
const int LOGO_Y = COPY_Y - mini_logo_->getHeight() - LOGO_GAP_PX;
|
||||
mini_logo_->render(LOGO_X, LOGO_Y);
|
||||
// copyright. El text va al `title.copyright_y_percent`, el logo just
|
||||
// damunt amb un petit gap (`title.mini_logo_gap_px`). El copyright fa
|
||||
// servir una font pixel-art pròpia (Tiny5-Regular size 8) — més neta
|
||||
// i petita que la del "press start" perquè la jerarquia tipogràfica
|
||||
// del peu quedi clara.
|
||||
if (!copyright_font_) { return; }
|
||||
|
||||
const auto& ui_config = UiConfig::get();
|
||||
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,
|
||||
COPY_Y,
|
||||
y,
|
||||
"© 2026 JailDesigner",
|
||||
condensed_font_->getDefaultKerning() - 1,
|
||||
copyright_font_->getDefaultKerning(),
|
||||
ui_config.titleCopyrightColor());
|
||||
}
|
||||
}
|
||||
|
||||
Notifier::get().render();
|
||||
|
||||
void TitleScene::renderTransitions() {
|
||||
// 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
|
||||
// condicional. Va per damunt del Notifier — coherent amb la resta
|
||||
// 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())) {
|
||||
fade_out_->render();
|
||||
}
|
||||
|
||||
RenderInfo::get().render();
|
||||
Screen::get().present();
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "core/rendering/transitions/fade_to_black.hpp"
|
||||
#include "core/rendering/transitions/mosaic.hpp"
|
||||
#include "core/scene/scene.hpp"
|
||||
#include "core/ui/ui_config.hpp"
|
||||
#include "game/debug_config.hpp"
|
||||
|
||||
class Surface;
|
||||
@@ -34,7 +35,7 @@ class TitleScene : public Scene {
|
||||
STARTING,
|
||||
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;
|
||||
|
||||
void update(int delta_ms) override;
|
||||
@@ -49,12 +50,29 @@ class TitleScene : public Scene {
|
||||
void advanceToIntro(DebugConfig::StartingPlayers players);
|
||||
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> copyright_font_;
|
||||
std::shared_ptr<Surface> background_;
|
||||
std::shared_ptr<Surface> game_logo_;
|
||||
std::shared_ptr<Surface> mini_logo_;
|
||||
int timer_ms_;
|
||||
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_;
|
||||
Phase phase_{Phase::IDLE};
|
||||
int starting_timer_ms_{0};
|
||||
@@ -62,7 +80,7 @@ class TitleScene : public Scene {
|
||||
std::optional<Transitions::Mosaic> fade_out_;
|
||||
std::optional<Transitions::FadeToBlack> attract_fade_;
|
||||
// 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
|
||||
// del cicle de l'escena.
|
||||
Transitions::FadeToBlack intro_fade_;
|
||||
|
||||
@@ -1168,12 +1168,18 @@ auto ServiceMenu::buildSystem() -> Page {
|
||||
page.items.push_back(makeLabel(
|
||||
std::string("ver.") + Texts::VERSION + " (" + Version::GIT_HASH + ")"));
|
||||
|
||||
// Reiniciar el joc: placeholder sense efecte fins que Director exposi
|
||||
// un pas de restart en calent (fora d'abast d'aquesta iteració).
|
||||
// Reiniciar el joc: tornem al LOGO amb `reset=true` perquè LogoScene
|
||||
// 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(
|
||||
"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(
|
||||
|
||||
@@ -6,11 +6,13 @@
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "core/input/define_buttons.hpp"
|
||||
#include "core/rendering/surface.hpp" // Palette
|
||||
#include "game/scenes/scene_flow.hpp"
|
||||
|
||||
class Text;
|
||||
|
||||
@@ -73,6 +75,19 @@ class ServiceMenu {
|
||||
// `define_buttons_->enable(...)` de coffee_crisis_arcade_edition.
|
||||
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:
|
||||
// --- Struct d'un item d'una pàgina ---
|
||||
//
|
||||
@@ -289,4 +304,9 @@ class ServiceMenu {
|
||||
// persisteix la nova configuració via `GamepadBindings`. Replica el
|
||||
// patró `define_buttons_` de coffee_crisis_arcade_edition.
|
||||
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_;
|
||||
};
|
||||
@@ -156,6 +156,16 @@ namespace Defaults {
|
||||
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 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
|
||||
|
||||
// Overlay de RenderInfo (F12).
|
||||
@@ -251,6 +261,13 @@ namespace Defaults {
|
||||
// `data/sounds/`.
|
||||
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
|
||||
// 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
|
||||
@@ -261,7 +278,22 @@ namespace Defaults {
|
||||
constexpr int DURATION_MS = 30000;
|
||||
constexpr Uint8 BG_COLOR = 1;
|
||||
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_HIDDEN_MS = 200;
|
||||
|
||||
@@ -294,12 +326,25 @@ namespace Defaults {
|
||||
// veurà què en fa.
|
||||
constexpr int ATTRACT_FADE_TO_BLACK_MS = 800;
|
||||
|
||||
// Fade-in inicial de l'escena: capa negra al començament que
|
||||
// s'esvaeix cap a la imatge de fons. Lineal, sense mosaic —
|
||||
// mateixa convenció que el `room_intro.fade_in_ms` del joc
|
||||
// perquè l'arribada a la TITLE i l'arribada a una sala de
|
||||
// joc tinguin el mateix ritme visual.
|
||||
constexpr int INTRO_FADE_IN_MS = 500;
|
||||
// Seqüència d'aparició dels elements de la TITLE en fase IDLE.
|
||||
// Tots els temps són absoluts des de l'entrada a l'escena.
|
||||
// `FADE_IN_MS` és la capa negra inicial que s'esvaeix cap al fons
|
||||
// (mateixa convenció que `room_intro.fade_in_ms` del joc perquè
|
||||
// l'arribada a la TITLE i a una sala de joc tinguin el mateix
|
||||
// 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
|
||||
// (efecte de calor del desert). `amplitude_px` és el
|
||||
@@ -314,6 +359,43 @@ namespace Defaults {
|
||||
constexpr float SPEED_HZ = 0.6F;
|
||||
} // namespace HeatHaze
|
||||
} // 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
|
||||
|
||||
#ifdef _DEBUG
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
namespace Texts {
|
||||
constexpr const char* WINDOW_TITLE = "© 2026 Aventures en Egipte ARCADE — 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 GameCanvas {
|
||||
|
||||
Reference in new issue
Block a user