#include "game/ui/hud.hpp" #include #include #include #include #include #include #include #include #include #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(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 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(&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(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(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(b.entry_ms_left) / static_cast(ENTRY_MS)); const float E = Easing::outCubic(T); const float REMAIN = 1.0F - E; // 1 al principi → 0 al final off.x += static_cast(b.entry_offset_x) * REMAIN; off.y += static_cast(b.entry_offset_y) * REMAIN; } // Shake: amplitud decau linealment amb el temps restant. if (b.shake_ms_left > 0) { const float DECAY = static_cast(b.shake_ms_left) / static_cast(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(id); const auto& b = blocks_[IDX]; const auto OFF = computeOffset(b); const int X = b.x + static_cast(std::round(OFF.x)); const int Y = b.y + static_cast(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(player_index)]; const Uint8 SCORE_C = (blk.flash_ms_left > 0) ? FLASH_COLOR : VALUE_COLOR; // --- Línia 1: "