feat(hud): marcador de la franja superior amb 3 blocs i animació d'entrada
This commit is contained in:
1 parent
67ecb704d2
commit
82b229222f
6 files changed
+474
-1
No files matched your search
@@ -84,6 +84,7 @@ set(APP_SOURCES
|
|||||||
source/game/options.cpp
|
source/game/options.cpp
|
||||||
source/game/options_apply.cpp
|
source/game/options_apply.cpp
|
||||||
source/game/ui/service_menu.cpp
|
source/game/ui/service_menu.cpp
|
||||||
|
source/game/ui/hud.cpp
|
||||||
source/game/scenes/placeholder_input.cpp
|
source/game/scenes/placeholder_input.cpp
|
||||||
source/game/scenes/logo_scene.cpp
|
source/game/scenes/logo_scene.cpp
|
||||||
source/game/scenes/title_scene.cpp
|
source/game/scenes/title_scene.cpp
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
#include "game/entities/player.hpp"
|
#include "game/entities/player.hpp"
|
||||||
#include "game/map/map.hpp"
|
#include "game/map/map.hpp"
|
||||||
#include "game/options.hpp"
|
#include "game/options.hpp"
|
||||||
|
#include "game/ui/hud.hpp"
|
||||||
#include "game/ui/ui_palette.hpp"
|
#include "game/ui/ui_palette.hpp"
|
||||||
#include "utils/defaults.hpp"
|
#include "utils/defaults.hpp"
|
||||||
#include "utils/defines.hpp"
|
#include "utils/defines.hpp"
|
||||||
@@ -139,6 +140,16 @@ GameScene::GameScene(Config config)
|
|||||||
// sarcòfags passa per `tile_painted`.
|
// sarcòfags passa per `tile_painted`.
|
||||||
players_.back()->setEventBus(&entity_manager_.getBus());
|
players_.back()->setEventBus(&entity_manager_.getBus());
|
||||||
|
|
||||||
|
// Marcador (HUD) de la franja superior. Reutilitza la font 8bithud i
|
||||||
|
// la paleta "ui" ja precarregades pel Director — no toca disc des
|
||||||
|
// d'aquí. Subscrit al bus per reaccionar a `player_hit`,
|
||||||
|
// `level_complete`, etc. amb shake/flash i futurs canvis de mode.
|
||||||
|
const std::string HUD_FONT_GIF_KEY = std::string(Defaults::UI::Font::HUD_NAME) + ".gif";
|
||||||
|
auto hud_font_surface = Resource::Cache::get().getSurface(HUD_FONT_GIF_KEY);
|
||||||
|
auto hud_font = std::make_shared<Text>(hud_font_surface, Defaults::UI::Font::HUD_FNT);
|
||||||
|
hud_font->setDefaultKerning(Defaults::UI::Font::HUD_KERNING);
|
||||||
|
hud_ = std::make_unique<Hud>(entity_manager_.getBus(), hud_font, Resource::Cache::get().getPalette("ui"));
|
||||||
|
|
||||||
// Música de fons en bucle infinit. El track ja està al Resource::Cache
|
// Música de fons en bucle infinit. El track ja està al Resource::Cache
|
||||||
// (preload al Director), així que playMusic només mou el punter cap a
|
// (preload al Director), així que playMusic només mou el punter cap a
|
||||||
// jail_audio i obri l'stream. Audio::get() està garantit no-null:
|
// jail_audio i obri l'stream. Audio::get() està garantit no-null:
|
||||||
@@ -236,6 +247,11 @@ void GameScene::update(int delta_ms) {
|
|||||||
editor_.update(delta_ms, tilemap_);
|
editor_.update(delta_ms, tilemap_);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// El HUD continua avançant les seues animacions encara que el món
|
||||||
|
// estigui congelat (pausa o editor): així l'animació d'entrada acaba
|
||||||
|
// sola i quan es despausa ja està al seu lloc, igual que el Notifier.
|
||||||
|
if (hud_) { hud_->update(delta_ms); }
|
||||||
|
|
||||||
RenderInfo::get().update(delta_ms, Options::buildRenderInfoFrame(UiColor::RENDER_INFO_TEXT, UiColor::RENDER_INFO_SHADOW));
|
RenderInfo::get().update(delta_ms, Options::buildRenderInfoFrame(UiColor::RENDER_INFO_TEXT, UiColor::RENDER_INFO_SHADOW));
|
||||||
Notifier::get().update(delta_ms);
|
Notifier::get().update(delta_ms);
|
||||||
}
|
}
|
||||||
@@ -291,6 +307,11 @@ void GameScene::render() {
|
|||||||
r.player->render();
|
r.player->render();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// HUD: a sobre dels actors perquè la franja superior tape qualsevol
|
||||||
|
// sprite que (durant l'animació d'entrada) puga sortir més amunt
|
||||||
|
// del playfield. S'amaga en mode editor (l'editor edita el tilemap;
|
||||||
|
// el HUD distrauria) però segueix actualitzant-se per fora.
|
||||||
|
if (hud_) { hud_->render(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef _DEBUG
|
#ifdef _DEBUG
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
#include "game/editor/editor.hpp"
|
#include "game/editor/editor.hpp"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
class Hud;
|
||||||
class Player;
|
class Player;
|
||||||
class Text;
|
class Text;
|
||||||
|
|
||||||
@@ -63,6 +64,14 @@ class GameScene : public Scene {
|
|||||||
// dos membres per garantir l'ordre invers a la destrucció.
|
// dos membres per garantir l'ordre invers a la destrucció.
|
||||||
std::unique_ptr<PaintLayer> paint_layer_;
|
std::unique_ptr<PaintLayer> paint_layer_;
|
||||||
|
|
||||||
|
// Marcador de la franja superior (24 px). `unique_ptr` per controlar
|
||||||
|
// l'ordre de construcció/destrucció igual que `paint_layer_`: es crea
|
||||||
|
// al ctor un cop `entity_manager_` ja té el bus, i es destrueix abans
|
||||||
|
// que `entity_manager_` (on viu el bus al qual està subscrit).
|
||||||
|
// Declarat després d'`entity_manager_` per garantir l'ordre invers a
|
||||||
|
// la destrucció.
|
||||||
|
std::unique_ptr<Hud> hud_;
|
||||||
|
|
||||||
// Pausa de gameplay. Quan `paused_` és true, l'update de jugadors i
|
// Pausa de gameplay. Quan `paused_` és true, l'update de jugadors i
|
||||||
// entitats es salta (el món es congela); l'input d'UI (GlobalInputs,
|
// entitats es salta (el món es congela); l'input d'UI (GlobalInputs,
|
||||||
// Notifier, RenderInfo) continua per no perdre hotkeys ni l'overlay de
|
// Notifier, RenderInfo) continua per no perdre hotkeys ni l'overlay de
|
||||||
|
|||||||
@@ -0,0 +1,278 @@
|
|||||||
|
#include "game/ui/hud.hpp"
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <iostream>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
#include <variant>
|
||||||
|
|
||||||
|
#include "core/rendering/screen.hpp"
|
||||||
|
#include "core/rendering/text.hpp"
|
||||||
|
#include "game/ui/ui_palette.hpp"
|
||||||
|
#include "utils/defaults.hpp"
|
||||||
|
#include "utils/defines.hpp"
|
||||||
|
#include "utils/easing.hpp"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Colors HUD reaprofitats de la paleta UI: les etiquetes ("P1", "V",
|
||||||
|
// "T") usen un to apagat per crear jerarquia visual respecte als
|
||||||
|
// valors. El flash temporal usa un to càlid (groc del títol del menú).
|
||||||
|
constexpr Uint8 LABEL_COLOR = UiColor::MENU_LABEL; // 47
|
||||||
|
constexpr Uint8 VALUE_COLOR = UiColor::MENU_VALUE; // 197
|
||||||
|
constexpr Uint8 BG_COLOR = UiColor::MENU_BG; // 40
|
||||||
|
constexpr Uint8 FLASH_COLOR = Defaults::UI::MENU_TITLE; // 213
|
||||||
|
|
||||||
|
// Amplada nominal d'una cel·la de dígit. Imposem monoespaiat als
|
||||||
|
// comptadors per evitar jitter visible quan canvien valors (Notifier
|
||||||
|
// ho fa igual amb `writeMonoDigits`).
|
||||||
|
constexpr int MONO_DIGIT_W = 6;
|
||||||
|
|
||||||
|
// Separadors visuals entre etiquetes i valors.
|
||||||
|
constexpr int LABEL_GAP = 2; // px entre "P1" i "000000"
|
||||||
|
constexpr int FIELD_GAP = 6; // px entre "V 3" i "T 00"
|
||||||
|
|
||||||
|
// Format zero-padded a 6 dígits per a la puntuació.
|
||||||
|
auto formatScore(int v) -> std::string {
|
||||||
|
const int CLAMPED = std::clamp(v, 0, 999999);
|
||||||
|
std::string s = std::to_string(CLAMPED);
|
||||||
|
return std::string(6 - s.size(), '0') + s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format zero-padded a 2 dígits per a comptadors petits (tresors,
|
||||||
|
// sales, vides quan en tenim moltes).
|
||||||
|
auto formatTwoDigits(int v) -> std::string {
|
||||||
|
const int CLAMPED = std::clamp(v, 0, 99);
|
||||||
|
std::string s = std::to_string(CLAMPED);
|
||||||
|
return std::string(2 - s.size(), '0') + s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vibració suau per al shake: dues sinusoides desfasades amb
|
||||||
|
// freqüències lleugerament diferents per a evitar el patró regular
|
||||||
|
// que faria una sola sinusoide. La fase ve dels ms restants — així
|
||||||
|
// queda determinista per frame, sense necessitat d'un PRNG.
|
||||||
|
auto shakeOffset(float intensity, int phase_ms) -> SDL_FPoint {
|
||||||
|
const float ANG = static_cast<float>(phase_ms) * 0.07F;
|
||||||
|
return SDL_FPoint{
|
||||||
|
.x = std::cos(ANG) * intensity,
|
||||||
|
.y = std::sin(ANG * 1.31F) * intensity,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Hud::Hud(EventBus& bus, std::shared_ptr<Text> font, const Palette& ui_palette)
|
||||||
|
: bus_(bus),
|
||||||
|
font_(std::move(font)),
|
||||||
|
ui_palette_(ui_palette) {
|
||||||
|
// Geometria fixa dels tres blocs. Sumen exactament GameCanvas::WIDTH.
|
||||||
|
blocks_[0] = Block{
|
||||||
|
.x = 0,
|
||||||
|
.y = 0,
|
||||||
|
.w = LEFT_W,
|
||||||
|
.h = HEIGHT_PX,
|
||||||
|
.entry_offset_x = -LEFT_W,
|
||||||
|
.entry_offset_y = 0,
|
||||||
|
};
|
||||||
|
blocks_[1] = Block{
|
||||||
|
.x = LEFT_W,
|
||||||
|
.y = 0,
|
||||||
|
.w = CENTER_W,
|
||||||
|
.h = HEIGHT_PX,
|
||||||
|
.entry_offset_x = 0,
|
||||||
|
.entry_offset_y = -HEIGHT_PX,
|
||||||
|
};
|
||||||
|
blocks_[2] = Block{
|
||||||
|
.x = LEFT_W + CENTER_W,
|
||||||
|
.y = 0,
|
||||||
|
.w = RIGHT_W,
|
||||||
|
.h = HEIGHT_PX,
|
||||||
|
.entry_offset_x = RIGHT_W,
|
||||||
|
.entry_offset_y = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Per defecte, P1 actiu (sempre n'hi ha al menys un); P2 inactiu fins
|
||||||
|
// que un segon Player s'uneix mitjançant `activatePlayer(2)`.
|
||||||
|
players_[0].active = true;
|
||||||
|
players_[1].active = false;
|
||||||
|
|
||||||
|
// Subscripcions al bus de l'EntityManager. De moment només `player_hit`
|
||||||
|
// afecta el HUD visualment: decrementa vides, dispara flash i shake al
|
||||||
|
// bloc del jugador. Quan els events evolucionin per portar `player_id`
|
||||||
|
// i puntuacions, ampliarem aquí sense tocar callers.
|
||||||
|
subs_.push_back(bus_.subscribe("player_hit", [this](const Event& ev) {
|
||||||
|
const auto* dmg = std::get_if<int>(&ev.payload);
|
||||||
|
// TODO: quan l'event porte player_id, fer dispatch al bloc real.
|
||||||
|
// Mentrestant, apliquem el hit a P1 (consistent amb el log del
|
||||||
|
// GameScene actual, que tampoc desambigua el jugador).
|
||||||
|
constexpr std::size_t IDX = 0;
|
||||||
|
if (players_[IDX].lives > 0) { --players_[IDX].lives; }
|
||||||
|
auto& blk = blocks_[IDX]; // P1 → bloc esquerre
|
||||||
|
blk.flash_ms_left = FLASH_MS;
|
||||||
|
blk.shake_ms_left = SHAKE_MS;
|
||||||
|
blk.shake_intensity = 1.5F;
|
||||||
|
std::cout << "[HUD] player_hit dmg=" << (dmg != nullptr ? *dmg : 0)
|
||||||
|
<< " lives=" << players_[IDX].lives << '\n';
|
||||||
|
}));
|
||||||
|
|
||||||
|
subs_.push_back(bus_.subscribe("level_complete", [](const Event& /*ev*/) {
|
||||||
|
// TODO: capturar `this` i cridar setMode(*, Mode::Win) per als
|
||||||
|
// tres blocs quan el mode Win estiga implementat.
|
||||||
|
std::cout << "[HUD] level_complete (mode Win pendent)\n";
|
||||||
|
}));
|
||||||
|
|
||||||
|
std::cout << "[HUD] init: 3 blocks ready (entry " << ENTRY_MS << " ms)\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
Hud::~Hud() {
|
||||||
|
for (auto id : subs_) { bus_.unsubscribe(id); }
|
||||||
|
}
|
||||||
|
|
||||||
|
void Hud::update(int delta_ms) {
|
||||||
|
for (auto& b : blocks_) {
|
||||||
|
if (b.entry_ms_left > 0) { b.entry_ms_left = std::max(0, b.entry_ms_left - delta_ms); }
|
||||||
|
if (b.shake_ms_left > 0) { b.shake_ms_left = std::max(0, b.shake_ms_left - delta_ms); }
|
||||||
|
if (b.flash_ms_left > 0) { b.flash_ms_left = std::max(0, b.flash_ms_left - delta_ms); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Hud::setMode(BlockId block, Mode mode) {
|
||||||
|
blocks_[static_cast<std::size_t>(block)].mode = mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Hud::setPyramidRoom(int pyramid, int room) {
|
||||||
|
pyramid_ = pyramid;
|
||||||
|
room_ = room;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Hud::activatePlayer(int player_one_based) {
|
||||||
|
if (player_one_based < 1 || player_one_based > 2) { return; }
|
||||||
|
auto& p = players_[static_cast<std::size_t>(player_one_based - 1)];
|
||||||
|
p.active = true;
|
||||||
|
p.lives = DEFAULT_LIVES;
|
||||||
|
p.score = 0;
|
||||||
|
p.treasures = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Hud::computeOffset(const Block& b) -> SDL_FPoint {
|
||||||
|
SDL_FPoint off{.x = 0.0F, .y = 0.0F};
|
||||||
|
|
||||||
|
// Animació d'entrada: t va de 0 (recent iniciat) a 1 (acabada). Amb
|
||||||
|
// outCubic, el bloc arrenca ràpid i s'atura suaument al seu lloc.
|
||||||
|
if (b.entry_ms_left > 0) {
|
||||||
|
const float T = 1.0F - (static_cast<float>(b.entry_ms_left) / static_cast<float>(ENTRY_MS));
|
||||||
|
const float E = Easing::outCubic(T);
|
||||||
|
const float REMAIN = 1.0F - E; // 1 al principi → 0 al final
|
||||||
|
off.x += static_cast<float>(b.entry_offset_x) * REMAIN;
|
||||||
|
off.y += static_cast<float>(b.entry_offset_y) * REMAIN;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shake: amplitud decau linealment amb el temps restant.
|
||||||
|
if (b.shake_ms_left > 0) {
|
||||||
|
const float DECAY = static_cast<float>(b.shake_ms_left) / static_cast<float>(SHAKE_MS);
|
||||||
|
const auto JITTER = shakeOffset(b.shake_intensity * DECAY, b.shake_ms_left);
|
||||||
|
off.x += JITTER.x;
|
||||||
|
off.y += JITTER.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
return off;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Hud::render() const {
|
||||||
|
renderBlock(BlockId::LEFT);
|
||||||
|
renderBlock(BlockId::CENTER);
|
||||||
|
renderBlock(BlockId::RIGHT);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Hud::renderBlock(BlockId id) const {
|
||||||
|
const auto IDX = static_cast<std::size_t>(id);
|
||||||
|
const auto& b = blocks_[IDX];
|
||||||
|
const auto OFF = computeOffset(b);
|
||||||
|
const int X = b.x + static_cast<int>(std::round(OFF.x));
|
||||||
|
const int Y = b.y + static_cast<int>(std::round(OFF.y));
|
||||||
|
|
||||||
|
// Fons sòlid del bloc (mateixa convenció que `Notifier::render`):
|
||||||
|
// ARGB directe al canvas, sense tocar surfaces paletades.
|
||||||
|
Screen::get().fillRect(X, Y, b.w, b.h, ui_palette_[BG_COLOR]);
|
||||||
|
|
||||||
|
switch (b.mode) {
|
||||||
|
case Mode::IDLE:
|
||||||
|
if (id == BlockId::LEFT) {
|
||||||
|
renderIdleSide(X, Y, b.w, b, 0, "P1");
|
||||||
|
} else if (id == BlockId::RIGHT) {
|
||||||
|
renderIdleSide(X, Y, b.w, b, 1, "P2");
|
||||||
|
} else {
|
||||||
|
renderIdleCenter(X, Y, b.w);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Mode::DYING: // TODO: animació de "vida perduda".
|
||||||
|
case Mode::COUNTDOWN: // TODO: 3-2-1 abans de respawn.
|
||||||
|
case Mode::WIN: // TODO: celebració al `level_complete`.
|
||||||
|
case Mode::EMPTY: // Deixa el bloc amb només el fons.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Hud::renderIdleSide(int x, int y, int w, const Block& blk, int player_index, const char* label) const {
|
||||||
|
const auto& p = players_[static_cast<std::size_t>(player_index)];
|
||||||
|
const Uint8 SCORE_C = (blk.flash_ms_left > 0) ? FLASH_COLOR : VALUE_COLOR;
|
||||||
|
|
||||||
|
// --- Línia 1: "<label> 000000" centrada ---
|
||||||
|
const std::string LBL{label};
|
||||||
|
const std::string SCORE = formatScore(p.score);
|
||||||
|
const int LBL_W = font_->length(LBL);
|
||||||
|
const int SC_W = font_->lengthMonoDigits(SCORE, MONO_DIGIT_W);
|
||||||
|
const int LINE1_W = LBL_W + LABEL_GAP + SC_W;
|
||||||
|
int xx = x + ((w - LINE1_W) / 2);
|
||||||
|
font_->writeColored(xx, y + LINE1_Y, LBL, LABEL_COLOR);
|
||||||
|
xx += LBL_W + LABEL_GAP;
|
||||||
|
font_->writeMonoDigits(xx, y + LINE1_Y, SCORE, SCORE_C, MONO_DIGIT_W);
|
||||||
|
|
||||||
|
// --- Línia 2: "V <vides> T <tresors>" centrada ---
|
||||||
|
const std::string V_LBL{"V"};
|
||||||
|
const std::string T_LBL{"T"};
|
||||||
|
const std::string V_VAL = std::to_string(std::clamp(p.lives, 0, 9));
|
||||||
|
const std::string T_VAL = formatTwoDigits(p.treasures);
|
||||||
|
const int V_LW = font_->length(V_LBL);
|
||||||
|
const int V_VW = font_->lengthMonoDigits(V_VAL, MONO_DIGIT_W);
|
||||||
|
const int T_LW = font_->length(T_LBL);
|
||||||
|
const int T_VW = font_->lengthMonoDigits(T_VAL, MONO_DIGIT_W);
|
||||||
|
const int LINE2_W = V_LW + LABEL_GAP + V_VW + FIELD_GAP + T_LW + LABEL_GAP + T_VW;
|
||||||
|
xx = x + ((w - LINE2_W) / 2);
|
||||||
|
font_->writeColored(xx, y + LINE2_Y, V_LBL, LABEL_COLOR);
|
||||||
|
xx += V_LW + LABEL_GAP;
|
||||||
|
font_->writeMonoDigits(xx, y + LINE2_Y, V_VAL, VALUE_COLOR, MONO_DIGIT_W);
|
||||||
|
xx += V_VW + FIELD_GAP;
|
||||||
|
font_->writeColored(xx, y + LINE2_Y, T_LBL, LABEL_COLOR);
|
||||||
|
xx += T_LW + LABEL_GAP;
|
||||||
|
font_->writeMonoDigits(xx, y + LINE2_Y, T_VAL, VALUE_COLOR, MONO_DIGIT_W);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Hud::renderIdleCenter(int x, int y, int w) const {
|
||||||
|
// --- Línia 1: "PIRAMIDE <n>" centrada ---
|
||||||
|
const std::string PIR_LBL{"PIRAMIDE"};
|
||||||
|
const std::string PIR_VAL = std::to_string(pyramid_);
|
||||||
|
const int PL_W = font_->length(PIR_LBL);
|
||||||
|
const int PV_W = font_->lengthMonoDigits(PIR_VAL, MONO_DIGIT_W);
|
||||||
|
const int LINE1_W = PL_W + LABEL_GAP + PV_W;
|
||||||
|
int xx = x + ((w - LINE1_W) / 2);
|
||||||
|
font_->writeColored(xx, y + LINE1_Y, PIR_LBL, LABEL_COLOR);
|
||||||
|
xx += PL_W + LABEL_GAP;
|
||||||
|
font_->writeMonoDigits(xx, y + LINE1_Y, PIR_VAL, VALUE_COLOR, MONO_DIGIT_W);
|
||||||
|
|
||||||
|
// --- Línia 2: "SALA <r>" centrada ---
|
||||||
|
const std::string SALA_LBL{"SALA"};
|
||||||
|
const std::string SALA_VAL = std::to_string(room_);
|
||||||
|
const int SL_W = font_->length(SALA_LBL);
|
||||||
|
const int SV_W = font_->lengthMonoDigits(SALA_VAL, MONO_DIGIT_W);
|
||||||
|
const int LINE2_W = SL_W + LABEL_GAP + SV_W;
|
||||||
|
xx = x + ((w - LINE2_W) / 2);
|
||||||
|
font_->writeColored(xx, y + LINE2_Y, SALA_LBL, LABEL_COLOR);
|
||||||
|
xx += SL_W + LABEL_GAP;
|
||||||
|
font_->writeMonoDigits(xx, y + LINE2_Y, SALA_VAL, VALUE_COLOR, MONO_DIGIT_W);
|
||||||
|
}
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "core/rendering/surface.hpp" // Palette
|
||||||
|
#include "game/entities/event_bus.hpp"
|
||||||
|
|
||||||
|
class Text;
|
||||||
|
|
||||||
|
// Marcador (HUD) de la GameScene. Ocupa la franja superior de 24 px del
|
||||||
|
// canvas i es divideix en tres "blocs" simètrics (P1 · COMÚ · P2) que
|
||||||
|
// funcionen com a panells independents — estil panell de pinball: cada un
|
||||||
|
// té el seu propi mode visible, animació d'entrada, shake i flash.
|
||||||
|
//
|
||||||
|
// Vida: instanciat per `GameScene` via `unique_ptr` (mateix patró que
|
||||||
|
// `PaintLayer`). Cada partida nova crea un HUD net; el destructor
|
||||||
|
// desubscriu del bus automàticament. **No és singleton** — és estat de
|
||||||
|
// partida, no configuració.
|
||||||
|
//
|
||||||
|
// Comunicació: subscrit al `EventBus` de l'`EntityManager`. Reacciona a
|
||||||
|
// events del joc (`player_hit`, `level_complete`, ...) sense que les
|
||||||
|
// entitats hagin de saber que existeix.
|
||||||
|
class Hud {
|
||||||
|
public:
|
||||||
|
// Identificador semàntic de bloc (també índex dins `blocks_`).
|
||||||
|
enum class BlockId : std::uint8_t { LEFT = 0,
|
||||||
|
CENTER = 1,
|
||||||
|
RIGHT = 2 };
|
||||||
|
|
||||||
|
// Mode actual d'un bloc. Tret d'`IDLE`, els altres modes són
|
||||||
|
// *placeholders* per a la primera versió (el render fa res); la API
|
||||||
|
// ja està fixada perquè afegir-los més endavant no toqui callers.
|
||||||
|
enum class Mode : std::uint8_t { IDLE,
|
||||||
|
DYING,
|
||||||
|
COUNTDOWN,
|
||||||
|
WIN,
|
||||||
|
EMPTY };
|
||||||
|
|
||||||
|
Hud(EventBus& bus, std::shared_ptr<Text> font, const Palette& ui_palette);
|
||||||
|
~Hud();
|
||||||
|
|
||||||
|
Hud(const Hud&) = delete;
|
||||||
|
Hud(Hud&&) = delete;
|
||||||
|
auto operator=(const Hud&) -> Hud& = delete;
|
||||||
|
auto operator=(Hud&&) -> Hud& = delete;
|
||||||
|
|
||||||
|
// Cada frame: avança les animacions actives (entrada, shake, flash).
|
||||||
|
void update(int delta_ms);
|
||||||
|
|
||||||
|
// Pinta els tres blocs sobre el canvas en la seva posició actual.
|
||||||
|
// El caller decideix l'ordre respecte a la resta de capes (en general
|
||||||
|
// cal cridar-lo a sobre del tilemap perquè quedi visible).
|
||||||
|
void render() const;
|
||||||
|
|
||||||
|
// Substitueix el mode d'un bloc (commutar el que es veu sense canviar
|
||||||
|
// la geometria — útil per a Dying/Countdown/Win quan estiguin fets).
|
||||||
|
void setMode(BlockId block, Mode mode);
|
||||||
|
|
||||||
|
// Actualitza l'identificador de piràmide i sala (apareix al bloc
|
||||||
|
// central). El caller en farà ús quan canvi de pantalla amb scroll o
|
||||||
|
// entre sales.
|
||||||
|
void setPyramidRoom(int pyramid, int room);
|
||||||
|
|
||||||
|
// Activa el bloc d'un jugador (1-based: 1 → P1, 2 → P2). Mentre està
|
||||||
|
// inactiu, el bloc mostra zeros i no rep flash/shake; quan s'activa,
|
||||||
|
// arrenca amb les vides per defecte.
|
||||||
|
void activatePlayer(int player_one_based);
|
||||||
|
|
||||||
|
// Geometria pública perquè la GameScene puga calcular si el HUD
|
||||||
|
// s'està movent encara (p. ex. retardar l'spawn fins que estiga al
|
||||||
|
// seu lloc) — opcional, no obligatori.
|
||||||
|
static constexpr int HEIGHT_PX = 24;
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Amplades dels tres blocs. Sumen exactament `GameCanvas::WIDTH` (320).
|
||||||
|
// Centrats: el bloc del mig agafa els píxels sobrants (108) i els
|
||||||
|
// laterals queden idèntics (106 cadascun) per garantir simetria.
|
||||||
|
static constexpr int LEFT_W = 106;
|
||||||
|
static constexpr int CENTER_W = 108;
|
||||||
|
static constexpr int RIGHT_W = 106;
|
||||||
|
|
||||||
|
// Files de text dins del bloc (y relatiu al bloc), per a una font de
|
||||||
|
// 8 px d'alçada amb un petit padding visual a dalt i a baix.
|
||||||
|
static constexpr int LINE1_Y = 4;
|
||||||
|
static constexpr int LINE2_Y = 13;
|
||||||
|
|
||||||
|
// Durada de l'animació d'entrada — els tres blocs alhora, amb easing
|
||||||
|
// outCubic. 400 ms és prou per a percebre el moviment sense fer
|
||||||
|
// esperar massa al jugador.
|
||||||
|
static constexpr int ENTRY_MS = 400;
|
||||||
|
|
||||||
|
// Durades per defecte d'efectes transitoris.
|
||||||
|
static constexpr int FLASH_MS = 250;
|
||||||
|
static constexpr int SHAKE_MS = 300;
|
||||||
|
|
||||||
|
// Vides inicials per jugador (placeholder fins que GameScene ho
|
||||||
|
// configure des del Config del nivell).
|
||||||
|
static constexpr int DEFAULT_LIVES = 3;
|
||||||
|
|
||||||
|
struct Block {
|
||||||
|
Mode mode{Mode::IDLE};
|
||||||
|
// Coordenades de pantalla on viu el bloc (constants durant la
|
||||||
|
// partida; només varien els offsets transitoris d'entrada/shake).
|
||||||
|
int x{0};
|
||||||
|
int y{0};
|
||||||
|
int w{0};
|
||||||
|
int h{0};
|
||||||
|
|
||||||
|
// Animació d'entrada: ms restants fins a quedar-se a la posició
|
||||||
|
// final. Quan val 0 el bloc està quiet.
|
||||||
|
int entry_ms_left{ENTRY_MS};
|
||||||
|
// Vector inicial des d'on entra: {-LEFT_W,0} per al bloc esquerre,
|
||||||
|
// {0,-HEIGHT_PX} per al central i {+RIGHT_W,0} per al dret.
|
||||||
|
int entry_offset_x{0};
|
||||||
|
int entry_offset_y{0};
|
||||||
|
|
||||||
|
// Shake actiu: amplitud i ms restants. Decau linealment.
|
||||||
|
int shake_ms_left{0};
|
||||||
|
float shake_intensity{0.0F};
|
||||||
|
|
||||||
|
// Flash actiu: mentre val >0, el text del valor es pinta amb el
|
||||||
|
// color de flash en compte del de valor.
|
||||||
|
int flash_ms_left{0};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PlayerStats {
|
||||||
|
int lives{DEFAULT_LIVES};
|
||||||
|
int score{0};
|
||||||
|
int treasures{0};
|
||||||
|
// `false` → el bloc mostra `000000`/`0`/`00` (jugador inactiu).
|
||||||
|
// Sempre es renderitza per mantenir el layout fix.
|
||||||
|
bool active{false};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Pinta el bloc indicat amb el seu fons + el contingut del seu mode
|
||||||
|
// actual. Aplica l'offset (entrada + shake) abans de dibuixar.
|
||||||
|
void renderBlock(BlockId id) const;
|
||||||
|
|
||||||
|
// Subrutines del mode `Idle` per als tres blocs. Reben el rectangle
|
||||||
|
// ja desplaçat (x, y) on han d'escriure.
|
||||||
|
void renderIdleSide(int x, int y, int w, const Block& blk, int player_index, const char* label) const;
|
||||||
|
void renderIdleCenter(int x, int y, int w) const;
|
||||||
|
|
||||||
|
// Calcula l'offset transitori (entrada + shake) en píxels per a un
|
||||||
|
// bloc. Static perquè depèn només dels camps del propi `Block` i de
|
||||||
|
// les constants de classe (no toca cap altre membre).
|
||||||
|
[[nodiscard]] static auto computeOffset(const Block& b) -> SDL_FPoint;
|
||||||
|
|
||||||
|
EventBus& bus_;
|
||||||
|
std::vector<EventBus::SubId> subs_;
|
||||||
|
|
||||||
|
std::shared_ptr<Text> font_;
|
||||||
|
Palette ui_palette_{};
|
||||||
|
|
||||||
|
std::array<Block, 3> blocks_;
|
||||||
|
std::array<PlayerStats, 2> players_;
|
||||||
|
int pyramid_{1};
|
||||||
|
int room_{1};
|
||||||
|
};
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
namespace Texts {
|
namespace Texts {
|
||||||
constexpr const char* WINDOW_TITLE = "© 2026 Aventures en Egipte — JailDesigner";
|
constexpr const char* WINDOW_TITLE = "© 2026 Aventures en Egipte — JailDesigner";
|
||||||
constexpr const char* COPYRIGHT = "© 2026 JailDesigner";
|
constexpr const char* COPYRIGHT = "© 2026 JailDesigner";
|
||||||
constexpr const char* VERSION = "0.21.0"; // Single source of truth — Makefile el grep
|
constexpr const char* VERSION = "0.22.0"; // Single source of truth — Makefile el grep
|
||||||
} // namespace Texts
|
} // namespace Texts
|
||||||
|
|
||||||
namespace GameCanvas {
|
namespace GameCanvas {
|
||||||
|
|||||||
Reference in new issue
Block a user