Merge branch 'feature/marcador': redisseny del marcador (color per jugador, ceros atenuats, vides com a slots/dígits commutables i layout centrat)
This commit is contained in:
@@ -10,7 +10,8 @@ namespace Defaults::Game {
|
||||
constexpr int HEIGHT = 720;
|
||||
|
||||
// Regles de partida
|
||||
constexpr int STARTING_LIVES = 3; // Initial lives
|
||||
constexpr int MAX_VIDES = 3; // Vides màximes per jugador (font única; el HUD en deriva els slots)
|
||||
constexpr int STARTING_LIVES = MAX_VIDES; // S'arrenca amb les vides al màxim
|
||||
constexpr float DEATH_DURATION = 3.0F; // Seconds of death animation
|
||||
constexpr float GAME_OVER_DURATION = 5.0F; // Seconds to display game over
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Defaults::Hud {
|
||||
|
||||
// Marcador (scoreboard inferior). Usado por GameScene::drawScoreboard()
|
||||
@@ -12,17 +14,42 @@ namespace Defaults::Hud {
|
||||
constexpr float SCOREBOARD_TEXT_SCALE = 0.85F;
|
||||
constexpr float SCOREBOARD_TEXT_SPACING = 0.0F;
|
||||
|
||||
// Colors per segment del marcador. Jerarquia per funció (score/vides/nivell)
|
||||
// + diferenciació de jugador (P1 blanc vs P2 rosa) sense xocar amb els
|
||||
// colors d'enemics (cyan/roig). Amb alpha=255 el line_renderer usa el color
|
||||
// directament sense caure al fallback verd (Rendering::DEFAULT_LINE_COLOR).
|
||||
// Mode de presentació de les vides al marcador (no es canvia en calent;
|
||||
// es defineix ací mentre no estiga decidit si el nombre de vides serà fix).
|
||||
// SLOTS → naus en miniatura en posicions fixes (s'encenen/atenuen).
|
||||
// DIGITS → número de 2 dígits (mateixa regla que el nivell: zeros a
|
||||
// l'esquerra atenuats, dígit significatiu en endavant encès).
|
||||
enum class LivesDisplay : std::uint8_t { SLOTS,
|
||||
DIGITS };
|
||||
constexpr LivesDisplay LIVES_DISPLAY = LivesDisplay::DIGITS;
|
||||
|
||||
// Ajust fi de l'alçada dels slots de vides respecte a l'alçada del glif del
|
||||
// dígit: la silueta de la nau ompli menys que un dígit, així que un xicotet
|
||||
// factor >1 la fa casar visualment amb les xifres (calibrat a ull).
|
||||
constexpr float LIFE_SLOT_HEIGHT_FACTOR = 1.2F;
|
||||
|
||||
// Esquema de color del marcador: "per jugador + sistema". Cada jugador usa
|
||||
// el SEU color (parella brillant/atenuat) en tot el seu bloc (punts + vides);
|
||||
// el nivell central va sempre en verd de sistema. Colors plans i purs: el
|
||||
// glow/bloom el posa el shader de postpro, NO s'horneja al color. Amb
|
||||
// alpha=255 el line_renderer usa el color directament sense caure al fallback
|
||||
// verd (Rendering::DEFAULT_LINE_COLOR).
|
||||
namespace Colors {
|
||||
constexpr SDL_Color SCORE_P1 = {.r = 255, .g = 255, .b = 255, .a = 255}; // blanc
|
||||
constexpr SDL_Color SCORE_P2 = {.r = 255, .g = 130, .b = 200, .a = 255}; // rosa magenta
|
||||
constexpr SDL_Color LIVES = {.r = 255, .g = 180, .b = 60, .a = 255}; // ambre / or
|
||||
constexpr SDL_Color LEVEL = {.r = 155, .g = 255, .b = 175, .a = 255}; // verd sistema
|
||||
// Jugador 1 → cian.
|
||||
constexpr SDL_Color P1_BRIGHT = {.r = 41, .g = 231, .b = 255, .a = 255}; // #29E7FF
|
||||
constexpr SDL_Color P1_DIM = {.r = 12, .g = 90, .b = 102, .a = 255}; // #0C5A66
|
||||
// Jugador 2 → groc.
|
||||
constexpr SDL_Color P2_BRIGHT = {.r = 255, .g = 226, .b = 58, .a = 255}; // #FFE23A
|
||||
constexpr SDL_Color P2_DIM = {.r = 90, .g = 82, .b = 16, .a = 255}; // #5A5210
|
||||
// Nivell / sistema → verd.
|
||||
constexpr SDL_Color LEVEL_BRIGHT = {.r = 77, .g = 255, .b = 102, .a = 255}; // #4DFF66
|
||||
constexpr SDL_Color LEVEL_DIM = {.r = 29, .g = 107, .b = 44, .a = 255}; // #1D6B2C
|
||||
} // namespace Colors
|
||||
|
||||
// Les vides es dibuixen com a slots fixos de naus en miniatura (NUM_SLOTS =
|
||||
// MAX_VIDES − 1). Mida i pas dels slots es deriven de la mètrica del glif del
|
||||
// dígit a init_hud_animator, no de constants soltes.
|
||||
|
||||
// Animación de entrada del HUD (init_hud_animator).
|
||||
namespace InitAnim {
|
||||
// Spawn vertical de la nave: 50 px bajo la PLAYAREA (sale desde fuera).
|
||||
|
||||
@@ -11,8 +11,9 @@
|
||||
namespace Graphics {
|
||||
|
||||
// Constants para mides base dels caràcters
|
||||
constexpr float BASE_CHAR_WIDTH = 20.0F; // Amplada base del caràcter
|
||||
constexpr float BASE_CHAR_HEIGHT = 40.0F; // Altura base del caràcter
|
||||
constexpr float BASE_CHAR_WIDTH = 20.0F; // Amplada base del caràcter (cel·la)
|
||||
constexpr float BASE_CHAR_HEIGHT = 40.0F; // Altura base del caràcter (cel·la, amb marge)
|
||||
constexpr float BASE_GLYPH_HEIGHT = 20.0F; // Altura real del glif (la majúscula/dígit ocupa 20 dels 40)
|
||||
|
||||
VectorText::VectorText(Rendering::Renderer* renderer)
|
||||
: renderer_(renderer) {
|
||||
@@ -287,4 +288,8 @@ namespace Graphics {
|
||||
return BASE_CHAR_HEIGHT * scale;
|
||||
}
|
||||
|
||||
auto VectorText::getGlyphHeight(float scale) -> float {
|
||||
return BASE_GLYPH_HEIGHT * scale;
|
||||
}
|
||||
|
||||
} // namespace Graphics
|
||||
|
||||
@@ -46,6 +46,10 @@ namespace Graphics {
|
||||
// Calcular altura del texto (útil para centrado vertical).
|
||||
[[nodiscard]] static auto getTextHeight(float scale = 1.0F) -> float;
|
||||
|
||||
// Altura real del glif (la majúscula/dígit, sense el marge vertical de la
|
||||
// cel·la). Útil per dimensionar icones que han de casar amb el text.
|
||||
[[nodiscard]] static auto getGlyphHeight(float scale = 1.0F) -> float;
|
||||
|
||||
// Verificar si un carácter está soportado
|
||||
[[nodiscard]] auto isSupported(char c) const -> bool;
|
||||
|
||||
|
||||
@@ -814,7 +814,7 @@ void GameScene::drawInitHudState() {
|
||||
}
|
||||
|
||||
if (score_progress > 0.0F) {
|
||||
Systems::InitHud::drawScoreboardAnimated(text_, buildScoreboardSegments(), score_progress);
|
||||
Systems::InitHud::drawScoreboardAnimated(sdl_.getRenderer(), text_, buildScoreboardData(), score_progress);
|
||||
}
|
||||
|
||||
if (ship1_progress > 0.0F && match_config_.player1_active && ships_[0].isActive()) {
|
||||
@@ -933,41 +933,36 @@ void GameScene::drawScoreboard() {
|
||||
text_.renderCentered(Locale::get().text("demo.banner"), CENTER, SCALE, SPACING);
|
||||
return;
|
||||
}
|
||||
Systems::InitHud::drawScoreboardSegmentsAt(text_, buildScoreboardSegments(), CENTER, SCALE, SPACING);
|
||||
Systems::InitHud::drawScoreboardAt(sdl_.getRenderer(), text_, buildScoreboardData(), CENTER.y, SCALE, SPACING);
|
||||
}
|
||||
|
||||
auto GameScene::buildScoreboardSegments() const -> Systems::InitHud::ScoreboardSegments {
|
||||
Systems::InitHud::ScoreboardSegments out;
|
||||
auto GameScene::buildScoreboardData() const -> Systems::InitHud::ScoreboardData {
|
||||
Systems::InitHud::ScoreboardData out;
|
||||
|
||||
// Puntuació P1 (6 dígits) - zeros si inactiu
|
||||
if (match_config_.player1_active) {
|
||||
std::string s = std::to_string(score_per_player_[0]);
|
||||
out.score_p1 = std::string(6 - std::min(6, static_cast<int>(s.length())), '0') + s;
|
||||
out.lives_p1 = (lives_per_player_[0] < 10)
|
||||
? "0" + std::to_string(lives_per_player_[0])
|
||||
: std::to_string(lives_per_player_[0]);
|
||||
} else {
|
||||
out.score_p1 = "000000";
|
||||
out.lives_p1 = "00";
|
||||
}
|
||||
// Puntuació a 6 dígits amb zeros a l'esquerra (inactiu → tot zeros, 0 vides).
|
||||
const auto FORMAT_SCORE = [](int score) {
|
||||
const std::string S = std::to_string(score);
|
||||
return std::string(6 - std::min(6, static_cast<int>(S.length())), '0') + S;
|
||||
};
|
||||
|
||||
// Nivell (2 dígits) amb label localitzat
|
||||
out.p1_active = match_config_.player1_active;
|
||||
out.p2_active = match_config_.player2_active;
|
||||
out.score_p1 = match_config_.player1_active ? FORMAT_SCORE(score_per_player_[0]) : "000000";
|
||||
out.lives_p1 = match_config_.player1_active ? lives_per_player_[0] : 0;
|
||||
out.score_p2 = match_config_.player2_active ? FORMAT_SCORE(score_per_player_[1]) : "000000";
|
||||
out.lives_p2 = match_config_.player2_active ? lives_per_player_[1] : 0;
|
||||
|
||||
// Shapes de les naus per a les icones de vides (reutilitza la geometria ja
|
||||
// carregada de cada Ship).
|
||||
out.shape_p1 = ships_[0].getShape();
|
||||
out.shape_p2 = ships_[1].getShape();
|
||||
|
||||
// Nivell: etiqueta localitzada + número a 2 dígits (separats per pintar-los
|
||||
// amb tonalitats distintes).
|
||||
const uint8_t STAGE_NUM = stage_manager_->getCurrentStage();
|
||||
const std::string STAGE_STR = (STAGE_NUM < 10) ? "0" + std::to_string(STAGE_NUM)
|
||||
: std::to_string(STAGE_NUM);
|
||||
out.level = Locale::get().text("hud.level") + STAGE_STR;
|
||||
|
||||
// Puntuació P2 (6 dígits) - zeros si inactiu
|
||||
if (match_config_.player2_active) {
|
||||
std::string s = std::to_string(score_per_player_[1]);
|
||||
out.score_p2 = std::string(6 - std::min(6, static_cast<int>(s.length())), '0') + s;
|
||||
out.lives_p2 = (lives_per_player_[1] < 10)
|
||||
? "0" + std::to_string(lives_per_player_[1])
|
||||
: std::to_string(lives_per_player_[1]);
|
||||
} else {
|
||||
out.score_p2 = "000000";
|
||||
out.lives_p2 = "00";
|
||||
}
|
||||
out.level_label = Locale::get().text("hud.level");
|
||||
out.level_value = (STAGE_NUM < 10) ? "0" + std::to_string(STAGE_NUM)
|
||||
: std::to_string(STAGE_NUM);
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
@@ -140,9 +140,9 @@ class GameScene final : public Scene {
|
||||
void drawPlayingState();
|
||||
void drawLevelCompletedState();
|
||||
|
||||
// [NEW] Helper del marcador: construeix els 5 segments (score_p1, vides_p1,
|
||||
// level, score_p2, vides_p2) per a render colorit per segment.
|
||||
[[nodiscard]] auto buildScoreboardSegments() const -> Systems::InitHud::ScoreboardSegments;
|
||||
// Helper del marcador: construeix les dades (puntuacions, vides i nivell)
|
||||
// per al render en blocs ancorats per jugador.
|
||||
[[nodiscard]] auto buildScoreboardData() const -> Systems::InitHud::ScoreboardData;
|
||||
|
||||
// Sub-pasos de update() (descompuestos en Fase 9d para reducir
|
||||
// complejidad cognitiva; cada uno es responsable de una sección).
|
||||
|
||||
@@ -5,12 +5,14 @@
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
|
||||
#include "core/defaults.hpp"
|
||||
#include "core/defaults/hud.hpp"
|
||||
#include "core/math/easing.hpp"
|
||||
#include "core/rendering/line_renderer.hpp"
|
||||
#include "core/rendering/shape_renderer.hpp"
|
||||
|
||||
namespace Systems::InitHud {
|
||||
|
||||
@@ -78,53 +80,255 @@ namespace Systems::InitHud {
|
||||
}
|
||||
}
|
||||
|
||||
void drawScoreboardSegmentsAt(const Graphics::VectorText& text,
|
||||
const ScoreboardSegments& segments,
|
||||
const Vec2& center,
|
||||
namespace {
|
||||
|
||||
// Nombre de slots de vides: una nau menys que el màxim (la nau en joc
|
||||
// no es dibuixa; els slots són repuestos). Deriva de MAX_VIDES.
|
||||
constexpr int NUM_SLOTS = Defaults::Game::MAX_VIDES - 1;
|
||||
|
||||
// Pas d'un dígit (amplada + tracking, escalat): és la diferència entre
|
||||
// l'ample de dos caràcters i el d'un. Marca el ritme de tot el bloc.
|
||||
auto digitPitch(float scale, float spacing) -> float {
|
||||
return Graphics::VectorText::getTextWidth("00", scale, spacing) -
|
||||
Graphics::VectorText::getTextWidth("0", scale, spacing);
|
||||
}
|
||||
|
||||
// Desplaçament vertical (unitats locals) entre el center declarat de la
|
||||
// shape i el centre real del seu bbox. La nau té center (0,0) però el seu
|
||||
// bbox no hi està centrat; cal per alinear el centre VISUAL de la nau amb
|
||||
// la línia del marcador (els dígits sí tenen el center al mig del glif).
|
||||
auto shapeVerticalOffset(const std::shared_ptr<Graphics::Shape>& shape) -> float {
|
||||
float min_y = std::numeric_limits<float>::max();
|
||||
float max_y = std::numeric_limits<float>::lowest();
|
||||
for (const auto& prim : shape->getPrimitives()) {
|
||||
for (const auto& point : prim.points) {
|
||||
min_y = std::min(min_y, point.y);
|
||||
max_y = std::max(max_y, point.y);
|
||||
}
|
||||
}
|
||||
return ((min_y + max_y) / 2.0F) - shape->getCenter().y;
|
||||
}
|
||||
|
||||
// Mida d'un slot = alçada real del glif del dígit (no la cel·la, que té
|
||||
// marge vertical: usar la cel·la feia les naus el doble de grans), amb un
|
||||
// xicotet factor d'ajust perquè la silueta de la nau case amb les xifres.
|
||||
auto slotSize(float scale) -> float {
|
||||
return Graphics::VectorText::getGlyphHeight(scale) * Defaults::Hud::LIFE_SLOT_HEIGHT_FACTOR;
|
||||
}
|
||||
|
||||
// Ample del bloc de slots: constant, independent de les vides. NUM_SLOTS
|
||||
// slots al pas del dígit (l'últim slot ocupa la seva pròpia mida).
|
||||
auto slotsBlockWidth(float scale, float spacing) -> float {
|
||||
if (NUM_SLOTS <= 0) {
|
||||
return 0.0F;
|
||||
}
|
||||
return (static_cast<float>(NUM_SLOTS - 1) * digitPitch(scale, spacing)) + slotSize(scale);
|
||||
}
|
||||
|
||||
// Vides com a número de 2 dígits (zeros a l'esquerra).
|
||||
auto livesDigits(int lives) -> std::string {
|
||||
const std::string S = std::to_string(lives);
|
||||
return (lives < 10) ? "0" + S : S;
|
||||
}
|
||||
|
||||
// Ample del bloc de vides segons el mode (constant en ambdós casos).
|
||||
auto livesBlockWidth(float scale, float spacing) -> float {
|
||||
if (Defaults::Hud::LIVES_DISPLAY == Defaults::Hud::LivesDisplay::DIGITS) {
|
||||
return Graphics::VectorText::getTextWidth("00", scale, spacing);
|
||||
}
|
||||
return slotsBlockWidth(scale, spacing);
|
||||
}
|
||||
|
||||
// Dibuixa els slots de vides com a naus en miniatura en posicions FIXES.
|
||||
// Slot amb vida disponible (repuesto) → color encès; slot buit → atenuat.
|
||||
// Repuestos = vides − 1 (la nau en joc no compta com a slot).
|
||||
void drawSlots(Rendering::Renderer* renderer,
|
||||
const std::shared_ptr<Graphics::Shape>& shape,
|
||||
int lives,
|
||||
SDL_Color bright,
|
||||
SDL_Color dim,
|
||||
float x_left,
|
||||
float center_y,
|
||||
float scale,
|
||||
float spacing) {
|
||||
if (NUM_SLOTS <= 0 || !shape) {
|
||||
return;
|
||||
}
|
||||
const float SIZE = slotSize(scale);
|
||||
const float PITCH = digitPitch(scale, spacing);
|
||||
// Escala que ajusta el cercle circumscrit de la shape a la mida del
|
||||
// slot (mida predictible independent del .shp).
|
||||
const float RADIUS = shape->getBoundingRadius();
|
||||
const float ICON_SCALE = (RADIUS > 0.001F) ? (SIZE / (2.0F * RADIUS)) : 1.0F;
|
||||
// Alinea el centre visual de la nau amb la línia del marcador.
|
||||
const float OFFSET_Y = shapeVerticalOffset(shape) * ICON_SCALE;
|
||||
const int FILLED = std::clamp(lives - 1, 0, NUM_SLOTS);
|
||||
for (int i = 0; i < NUM_SLOTS; i++) {
|
||||
const SDL_Color COLOR = (i < FILLED) ? bright : dim;
|
||||
const Vec2 POS = {.x = x_left + (SIZE / 2.0F) + (static_cast<float>(i) * PITCH), .y = center_y - OFFSET_Y};
|
||||
// glow=false: el marcador es manté net, com els dígits del text.
|
||||
Rendering::renderShape(renderer, shape, POS, 0.0F, ICON_SCALE, 1.0F, 1.0F, COLOR, 0.0F, 1.0F, false);
|
||||
}
|
||||
}
|
||||
|
||||
// Pinta la puntuació amb els zeros de farciment previs al primer dígit
|
||||
// significatiu en to atenuat i la resta en brillant (efecte display de 7
|
||||
// segments). El dígit menys significatiu SEMPRE va encès: puntuació 0 →
|
||||
// cinc zeros atenuats + l'últim "0" encès (el marcador no queda mai apagat).
|
||||
void drawScore(const Graphics::VectorText& text,
|
||||
const std::string& score,
|
||||
SDL_Color bright,
|
||||
SDL_Color dim,
|
||||
bool active,
|
||||
float x,
|
||||
float top_y,
|
||||
float scale,
|
||||
float spacing) {
|
||||
if (score.empty()) {
|
||||
return;
|
||||
}
|
||||
// Jugador inactiu → marcador apagat: tots els dígits atenuats (no té
|
||||
// "zero punts", simplement no en té). Jugador actiu → l'últim dígit
|
||||
// sempre encès (puntuació 0 → cinc zeros atenuats + "0" encès).
|
||||
const SDL_Color REST_COLOR = active ? bright : dim;
|
||||
const size_t FIRST_SIG = score.find_first_not_of('0');
|
||||
const size_t SIG = (FIRST_SIG == std::string::npos) ? (score.size() - 1) : FIRST_SIG;
|
||||
const std::string PREFIX = score.substr(0, SIG);
|
||||
const std::string REST = score.substr(SIG);
|
||||
if (!PREFIX.empty()) {
|
||||
text.render(PREFIX, {.x = x, .y = top_y}, scale, spacing, 1.0F, dim);
|
||||
// Avança l'amplada del prefix més el buit inter-caràcter que hi
|
||||
// hauria si fos un sol string (exacte per a qualsevol spacing).
|
||||
x += Graphics::VectorText::getTextWidth(PREFIX, scale, spacing) + (spacing * scale);
|
||||
}
|
||||
if (!REST.empty()) {
|
||||
text.render(REST, {.x = x, .y = top_y}, scale, spacing, 1.0F, REST_COLOR);
|
||||
}
|
||||
}
|
||||
|
||||
// Separació punts↔slots dins d'un bloc = un pas de dígit (ritme únic).
|
||||
auto blockInnerGap(float scale, float spacing) -> float {
|
||||
return digitPitch(scale, spacing);
|
||||
}
|
||||
|
||||
// Ample (constant) del bloc d'un jugador: 6 dígits + separació + vides.
|
||||
// No depèn de les vides, així res es recol·loca quan se'n perden.
|
||||
auto playerBlockWidth(float scale, float spacing) -> float {
|
||||
return Graphics::VectorText::getTextWidth("000000", scale, spacing) +
|
||||
blockInnerGap(scale, spacing) + livesBlockWidth(scale, spacing);
|
||||
}
|
||||
|
||||
// Pinta el bloc de vides segons el mode: slots de nau o número de 2 dígits.
|
||||
void drawLivesBlock(Rendering::Renderer* renderer,
|
||||
const Graphics::VectorText& text,
|
||||
const std::shared_ptr<Graphics::Shape>& shape,
|
||||
int lives,
|
||||
bool active,
|
||||
SDL_Color bright,
|
||||
SDL_Color dim,
|
||||
float x_left,
|
||||
float top_y,
|
||||
float center_y,
|
||||
float scale,
|
||||
float spacing) {
|
||||
if (Defaults::Hud::LIVES_DISPLAY == Defaults::Hud::LivesDisplay::DIGITS) {
|
||||
// Repuestos = vides − 1 (la nau en joc no compta), igual que els
|
||||
// slots. Mateixa regla de color que el nivell: zeros a l'esquerra
|
||||
// atenuats, dígit significatiu en endavant encès.
|
||||
const int SPARES = std::max(0, lives - 1);
|
||||
drawScore(text, livesDigits(SPARES), bright, dim, active, x_left, top_y, scale, spacing);
|
||||
return;
|
||||
}
|
||||
drawSlots(renderer, shape, lives, bright, dim, x_left, center_y, scale, spacing);
|
||||
}
|
||||
|
||||
// Pinta el bloc d'un jugador "punts vides" amb el seu color (punts amb
|
||||
// zeros atenuats, vides com a slots de nau o número segons el mode).
|
||||
// Ancorat a x_left (vora esquerra del bloc), mateix ordre per a P1 i P2
|
||||
// (no mirrored).
|
||||
void drawPlayerBlock(Rendering::Renderer* renderer,
|
||||
const Graphics::VectorText& text,
|
||||
const std::shared_ptr<Graphics::Shape>& shape,
|
||||
const std::string& score,
|
||||
int lives,
|
||||
bool active,
|
||||
SDL_Color bright,
|
||||
SDL_Color dim,
|
||||
float x_left,
|
||||
float center_y,
|
||||
float scale,
|
||||
float spacing) {
|
||||
// Jugador inactiu → bloc apagat: es dibuixa igual però tot atenuat
|
||||
// (punts i vides), com un display físic sense encendre.
|
||||
const float TOP_Y = center_y - (Graphics::VectorText::getTextHeight(scale) / 2.0F);
|
||||
const float W_SCORE = Graphics::VectorText::getTextWidth(score, scale, spacing);
|
||||
|
||||
float x = x_left;
|
||||
drawScore(text, score, bright, dim, active, x, TOP_Y, scale, spacing);
|
||||
x += W_SCORE + blockInnerGap(scale, spacing);
|
||||
drawLivesBlock(renderer, text, shape, lives, active, bright, dim, x, TOP_Y, center_y, scale, spacing);
|
||||
}
|
||||
|
||||
// Pinta el nivell centrat: etiqueta "NIVELL" encesa i el número com els
|
||||
// punts (zeros de farciment atenuats, dígit significatiu en endavant encès).
|
||||
void drawLevel(const Graphics::VectorText& text,
|
||||
const std::string& label,
|
||||
const std::string& value,
|
||||
float top_y,
|
||||
float scale,
|
||||
float spacing) {
|
||||
const float W_LABEL = Graphics::VectorText::getTextWidth(label, scale, spacing);
|
||||
const float W_VALUE = Graphics::VectorText::getTextWidth(value, scale, spacing);
|
||||
const float CX = Defaults::Game::WIDTH / 2.0F;
|
||||
float x = CX - ((W_LABEL + W_VALUE) / 2.0F);
|
||||
text.render(label, {.x = x, .y = top_y}, scale, spacing, 1.0F, Defaults::Hud::Colors::LEVEL_BRIGHT);
|
||||
x += W_LABEL;
|
||||
drawScore(text, value, Defaults::Hud::Colors::LEVEL_BRIGHT, Defaults::Hud::Colors::LEVEL_DIM, true, x, top_y, scale, spacing);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void drawScoreboardAt(Rendering::Renderer* renderer,
|
||||
const Graphics::VectorText& text,
|
||||
const ScoreboardData& data,
|
||||
float center_y,
|
||||
float scale,
|
||||
float spacing) {
|
||||
// Separadors entre segments (preservant el layout legacy: " ", " ", " ", " ").
|
||||
const float W_SEP1 = Graphics::VectorText::getTextWidth(" ", scale, spacing);
|
||||
const float W_SEP2 = Graphics::VectorText::getTextWidth(" ", scale, spacing);
|
||||
// Fila centrada amb posicions FIXES: [bloc P1] · [NIVELL] · [bloc P2].
|
||||
// Els blocs tenen ample constant (slots fixos), així NIVELL queda centrat
|
||||
// i res es recol·loca en perdre vides. Separadors derivats del glif
|
||||
// (dos espais), com el disseny original.
|
||||
const float TOP_Y = center_y - (Graphics::VectorText::getTextHeight(scale) / 2.0F);
|
||||
const float BLOCK_W = playerBlockWidth(scale, spacing);
|
||||
const float W_LEVEL = Graphics::VectorText::getTextWidth(data.level_label, scale, spacing) +
|
||||
Graphics::VectorText::getTextWidth(data.level_value, scale, spacing);
|
||||
const float GAP = Graphics::VectorText::getTextWidth(" ", scale, spacing);
|
||||
const float TOTAL = BLOCK_W + GAP + W_LEVEL + GAP + BLOCK_W;
|
||||
|
||||
const float W_SP1 = Graphics::VectorText::getTextWidth(segments.score_p1, scale, spacing);
|
||||
const float W_LP1 = Graphics::VectorText::getTextWidth(segments.lives_p1, scale, spacing);
|
||||
const float W_LV = Graphics::VectorText::getTextWidth(segments.level, scale, spacing);
|
||||
const float W_SP2 = Graphics::VectorText::getTextWidth(segments.score_p2, scale, spacing);
|
||||
const float W_LP2 = Graphics::VectorText::getTextWidth(segments.lives_p2, scale, spacing);
|
||||
|
||||
const float TOTAL = W_SP1 + W_SEP1 + W_LP1 + W_SEP2 + W_LV + W_SEP2 + W_SP2 + W_SEP1 + W_LP2;
|
||||
|
||||
const float HEIGHT = Graphics::VectorText::getTextHeight(scale);
|
||||
const float TOP_Y = center.y - (HEIGHT / 2.0F);
|
||||
float x = center.x - (TOTAL / 2.0F);
|
||||
|
||||
text.render(segments.score_p1, {.x = x, .y = TOP_Y}, scale, spacing, 1.0F, Defaults::Hud::Colors::SCORE_P1);
|
||||
x += W_SP1 + W_SEP1;
|
||||
text.render(segments.lives_p1, {.x = x, .y = TOP_Y}, scale, spacing, 1.0F, Defaults::Hud::Colors::LIVES);
|
||||
x += W_LP1 + W_SEP2;
|
||||
text.render(segments.level, {.x = x, .y = TOP_Y}, scale, spacing, 1.0F, Defaults::Hud::Colors::LEVEL);
|
||||
x += W_LV + W_SEP2;
|
||||
text.render(segments.score_p2, {.x = x, .y = TOP_Y}, scale, spacing, 1.0F, Defaults::Hud::Colors::SCORE_P2);
|
||||
x += W_SP2 + W_SEP1;
|
||||
text.render(segments.lives_p2, {.x = x, .y = TOP_Y}, scale, spacing, 1.0F, Defaults::Hud::Colors::LIVES);
|
||||
float x = (Defaults::Game::WIDTH / 2.0F) - (TOTAL / 2.0F);
|
||||
drawPlayerBlock(renderer, text, data.shape_p1, data.score_p1, data.lives_p1, data.p1_active, Defaults::Hud::Colors::P1_BRIGHT, Defaults::Hud::Colors::P1_DIM, x, center_y, scale, spacing);
|
||||
x += BLOCK_W + GAP;
|
||||
// NIVELL: drawLevel centra a WIDTH/2, que coincideix amb aquest tram.
|
||||
drawLevel(text, data.level_label, data.level_value, TOP_Y, scale, spacing);
|
||||
x += W_LEVEL + GAP;
|
||||
drawPlayerBlock(renderer, text, data.shape_p2, data.score_p2, data.lives_p2, data.p2_active, Defaults::Hud::Colors::P2_BRIGHT, Defaults::Hud::Colors::P2_DIM, x, center_y, scale, spacing);
|
||||
}
|
||||
|
||||
void drawScoreboardAnimated(const Graphics::VectorText& text,
|
||||
const ScoreboardSegments& segments,
|
||||
void drawScoreboardAnimated(Rendering::Renderer* renderer,
|
||||
const Graphics::VectorText& text,
|
||||
const ScoreboardData& data,
|
||||
float progress) {
|
||||
const float EASED = Easing::easeOutQuad(progress);
|
||||
|
||||
constexpr float SCALE = Defaults::Hud::SCOREBOARD_TEXT_SCALE;
|
||||
constexpr float SPACING = Defaults::Hud::SCOREBOARD_TEXT_SPACING;
|
||||
const SDL_FRect& scoreboard_zone = Defaults::Zones::SCOREBOARD;
|
||||
const float CENTRE_X = scoreboard_zone.w / 2.0F;
|
||||
const float Y_FINAL = scoreboard_zone.y + (scoreboard_zone.h / 2.0F);
|
||||
// Posición inicial: fuera de la pantalla por debajo.
|
||||
const auto Y_INI = static_cast<float>(Defaults::Game::HEIGHT);
|
||||
const float Y_ANIM = Y_INI + ((Y_FINAL - Y_INI) * EASED);
|
||||
|
||||
drawScoreboardSegmentsAt(text, segments, {.x = CENTRE_X, .y = Y_ANIM}, SCALE, SPACING);
|
||||
drawScoreboardAt(renderer, text, data, Y_ANIM, SCALE, SPACING);
|
||||
}
|
||||
|
||||
} // namespace Systems::InitHud
|
||||
|
||||
@@ -13,23 +13,32 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "core/graphics/shape.hpp"
|
||||
#include "core/graphics/vector_text.hpp"
|
||||
#include "core/rendering/render_context.hpp"
|
||||
#include "core/types.hpp"
|
||||
|
||||
namespace Systems::InitHud {
|
||||
|
||||
// Segments del marcador. Cada segment es renderitza amb el seu propi color
|
||||
// (vegeu Defaults::Hud::Colors). El layout final concatena en aquest ordre
|
||||
// amb separadors d'1, 2, 2, 1 espais respectivament (igual que el legacy).
|
||||
struct ScoreboardSegments {
|
||||
std::string score_p1;
|
||||
std::string lives_p1;
|
||||
std::string level; // ex: "NIVELL 01"
|
||||
std::string score_p2;
|
||||
std::string lives_p2;
|
||||
// Dades del marcador. El render reparteix tres blocs ancorats: bloc P1 a
|
||||
// l'esquerra i bloc P2 a la dreta (mateix ordre intern "punts vides", no
|
||||
// mirrored), i el nivell centrat. Cada bloc de jugador es pinta amb el seu
|
||||
// color (vegeu Defaults::Hud::Colors); el nivell, en verd de sistema.
|
||||
struct ScoreboardData {
|
||||
std::string score_p1; // 6 dígits, zeros a l'esquerra
|
||||
std::string score_p2; // 6 dígits, zeros a l'esquerra
|
||||
int lives_p1{0}; // vides P1 (icones de nau al render)
|
||||
int lives_p2{0}; // vides P2
|
||||
bool p1_active{false}; // jugador actiu? (inactiu → bloc apagat, sense dibuixar)
|
||||
bool p2_active{false};
|
||||
std::string level_label; // ex: "NIVELL "
|
||||
std::string level_value; // ex: "01"
|
||||
// Shapes de les naus per dibuixar les vides com a icones en miniatura.
|
||||
std::shared_ptr<Graphics::Shape> shape_p1;
|
||||
std::shared_ptr<Graphics::Shape> shape_p2;
|
||||
};
|
||||
|
||||
// Convierte un progreso global 0..1 al sub-progreso de un elemento que solo
|
||||
@@ -51,19 +60,22 @@ namespace Systems::InitHud {
|
||||
// 66..100% → línea inferior crece desde los lados hacia el centro.
|
||||
void drawBordersAnimated(Rendering::Renderer* renderer, float progress);
|
||||
|
||||
// Dibuixa els 5 segments del scoreboard centrats al voltant de `center`,
|
||||
// cadascun amb el seu color (Defaults::Hud::Colors). Separadors de 1/2/2/1
|
||||
// espais entre segments per preservar el layout legacy.
|
||||
void drawScoreboardSegmentsAt(const Graphics::VectorText& text,
|
||||
const ScoreboardSegments& segments,
|
||||
const Vec2& center,
|
||||
// Dibuixa el marcador en tres blocs ancorats a la fila d'alçada `center_y`:
|
||||
// bloc P1 a l'esquerra, bloc P2 a la dreta i nivell centrat. Cada bloc amb
|
||||
// el seu color (Defaults::Hud::Colors). El renderer cal per dibuixar les
|
||||
// icones de vides (shapes de nau).
|
||||
void drawScoreboardAt(Rendering::Renderer* renderer,
|
||||
const Graphics::VectorText& text,
|
||||
const ScoreboardData& data,
|
||||
float center_y,
|
||||
float scale,
|
||||
float spacing);
|
||||
|
||||
// Dibuixa el scoreboard centrat, pujant des de fora de la pantalla fins a
|
||||
// la seva posició final amb easing. Delega a drawScoreboardSegmentsAt.
|
||||
void drawScoreboardAnimated(const Graphics::VectorText& text,
|
||||
const ScoreboardSegments& segments,
|
||||
// Dibuixa el marcador pujant des de fora de la pantalla fins a la seva
|
||||
// posició final amb easing. Delega a drawScoreboardAt.
|
||||
void drawScoreboardAnimated(Rendering::Renderer* renderer,
|
||||
const Graphics::VectorText& text,
|
||||
const ScoreboardData& data,
|
||||
float progress);
|
||||
|
||||
} // namespace Systems::InitHud
|
||||
|
||||
Reference in New Issue
Block a user