From a254fdd669be2ffb7595c34f21649d1b03c08f2d Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Sat, 2 May 2026 12:42:47 +0200 Subject: [PATCH] feat(editor): hand pick per base, z-sort, amaga entitats en TILE, HUD 2-files centrat --- data/maps/p1/r1.yaml | 6 +- source/game/editor/editor.cpp | 94 ++++++++++++++++++++++--------- source/game/editor/editor.hpp | 8 +++ source/game/scenes/game_scene.cpp | 21 +++++-- 4 files changed, 94 insertions(+), 35 deletions(-) diff --git a/data/maps/p1/r1.yaml b/data/maps/p1/r1.yaml index 77d35c9..96e3874 100644 --- a/data/maps/p1/r1.yaml +++ b/data/maps/p1/r1.yaml @@ -133,8 +133,6 @@ entities: - type: door - type: mummy position: [10, 7] - - type: mummy - position: [7, 9] - type: hole position: [4, 7] - type: hole @@ -144,4 +142,6 @@ entities: - type: hole position: [15, 5] - type: hole - position: [6, 5] + position: [8, 5] + - type: mummy + position: [8, 9] diff --git a/source/game/editor/editor.cpp b/source/game/editor/editor.cpp index 6acd873..7d27bd4 100644 --- a/source/game/editor/editor.cpp +++ b/source/game/editor/editor.cpp @@ -6,11 +6,11 @@ #include #include +#include #include #include "core/input/key_config.hpp" #include "core/locale/locale.hpp" -#include "core/rendering/hud_constants.hpp" #include "core/rendering/notification_presets.hpp" #include "core/rendering/screen.hpp" #include "core/rendering/surface.hpp" @@ -180,14 +180,17 @@ namespace { void Editor::enter() { if (active_) { return; } active_ = true; - // Estat de novella: tornem a Mode::TILE sense brush ni picker. Els - // brushs (tile/entity) i la selecció de la mà no es preserven entre - // sessions del editor: cada entrada parteix net. - mode_ = Mode::TILE; + // Estat de novella: comencem en mode HAND (selecció/manipulació + // d'entitats existents) — el cas d'ús més habitual quan obres + // l'editor és inspeccionar/moure/borrar el que ja hi ha. Els + // brushs i la selecció no es preserven entre sessions: cada + // entrada parteix net. + mode_ = Mode::HAND; picker_ = Picker::NONE; brush_tile_ = -1; brush_entity_.clear(); hand_selected_idx_ = -1; + hand_hover_idx_ = -1; ensureEditableEntities(); ensureHudFont(); Notification::notify(Locale::get().text("input.editor_on")); @@ -544,15 +547,20 @@ void Editor::updateEntityBrush(EntityManager& entities, int /*canvas_x*/, int /* namespace { - // Index de l'entitat sota el píxel donat. -1 si cap. Usem el rect de - // pintat (`getPixelRect`) i no el de col·lisió perquè el dissenyador - // espera "fer clic on es veu", encara que el cos físic sigui més - // estret (típic en sarc/porta amb falsa perspectiva). + // Index de l'entitat sota el píxel donat. -1 si cap. Usem el rect + // de col·lisió (caixa base alineada al tilemap) i NO el sprite + // sencer: a entitats amb falsa perspectiva (mòmia 16×32, sarc + // 48×32) la part alta del sprite és "altura visual" que tapa + // cel·les transitables darrere. Si el hit-test usés el sprite + // sencer, no es podria seleccionar/col·locar res a aquelles + // cel·les. Per a entitats sense caixa explícita (torch, hole), + // `getCollisionRect` retorna el `pixel_rect` complet, així que el + // criteri segueix sent intuïtiu ("clic on es veu"). auto entityAtPixel(const std::vector& list, int px, int py) -> int { const auto FX = static_cast(px); const auto FY = static_cast(py); for (int i = static_cast(list.size()) - 1; i >= 0; --i) { - const auto R = list[i]->getPixelRect(); + const auto R = list[i]->getCollisionRect(); if (FX >= R.x && FX < R.x + R.w && FY >= R.y && FY < R.y + R.h) { return i; } } return -1; @@ -662,8 +670,13 @@ void Editor::deleteSelected(EntityManager& entities) { void Editor::renderEditorHud(const EntityManager& entities) const { if (hud_font_ == nullptr || Map::hud_h <= 0) { return; } auto& screen = Screen::get(); - const auto& pal = Resource::Cache::get().getPalette("ui"); - screen.fillRect(0, 0, GameCanvas::WIDTH, Map::hud_h, pal[UiConfig::get().hudBg()]); + + // Fons opcional: respectem el flag global del HUD del joc per a + // coherència visual quan el dissenyador alterna entre joc i editor. + if (UiConfig::get().hudBgEnabled()) { + const auto& pal = Resource::Cache::get().getPalette("ui"); + screen.fillRect(0, 0, GameCanvas::WIDTH, Map::hud_h, pal[UiConfig::get().hudBg()]); + } std::string mode_str; switch (mode_) { @@ -700,28 +713,55 @@ void Editor::renderEditorHud(const EntityManager& entities) const { const auto LBL = UiConfig::get().hudLabel(); const auto VAL = UiConfig::get().hudValue(); + const auto FLASH = UiConfig::get().hudFlash(); const int KERN = UiConfig::get().hudFont().kerning; - const int Y = (Map::hud_h - Rendering::Hud::FONT_SIZE_PX) / 2; - int x = 2; + // Mateixos Y que el HUD del joc per simetria visual: línia 1 = 4, + // línia 2 = 13. La font és proporcional, així que mesurem cada línia + // sencera abans de centrar-la. + constexpr int LINE1_Y = 4; + constexpr int LINE2_Y = 13; + constexpr int W = GameCanvas::WIDTH; - auto draw = [&](const std::string& s, std::uint8_t color) { - hud_font_->writeColored(x, Y, s, color, KERN); - x += hud_font_->length(s, KERN); + struct Segment { + std::string text; + std::uint8_t color; + }; + auto draw_line = [&](const std::vector& segs, int y) { + const int TOTAL = std::accumulate(segs.begin(), segs.end(), 0, [&](int acc, const Segment& s) { return acc + hud_font_->length(s.text, KERN); }); + int x = (W - TOTAL) / 2; + for (const auto& s : segs) { + hud_font_->writeColored(x, y, s.text, s.color, KERN); + x += hud_font_->length(s.text, KERN); + } }; - draw("MODE:", LBL); - draw(" " + mode_str, VAL); - draw(" " + brush_label, LBL); - draw(" " + brush_value, VAL); - draw(" " + COORD, VAL); - draw(" DOOR:", LBL); - draw(" " + std::to_string(countByName(entities, "door")), VAL); - draw(" SARCS:", LBL); - draw(" " + std::to_string(countByName(entities, "sarcophagus")), VAL); + // Línia 1: estat actiu (mode + brush/selecció + cursor). + draw_line({{.text = "MODE:", .color = LBL}, + {.text = " " + mode_str, .color = VAL}, + {.text = " " + brush_label, .color = LBL}, + {.text = " " + brush_value, .color = VAL}, + {.text = " " + COORD, .color = VAL}}, + LINE1_Y); + + // Línia 2: comptadors de constraints. Color "flash" del HUD del + // joc quan no es compleixen — mateix vocabulari visual que les + // alertes del marcador. + const int DOOR_C = countByName(entities, "door"); + const int SARC_C = countByName(entities, "sarcophagus"); + const auto DOOR_COLOR = (DOOR_C == 1) ? VAL : FLASH; + const auto SARC_COLOR = (SARC_C >= 1) ? VAL : FLASH; + draw_line({{.text = "DOOR:", .color = LBL}, + {.text = " " + std::to_string(DOOR_C), .color = DOOR_COLOR}, + {.text = " SARCS:", .color = LBL}, + {.text = " " + std::to_string(SARC_C), .color = SARC_COLOR}}, + LINE2_Y); } void Editor::renderEntityFrame(const Entity& e, std::uint32_t argb) { - const auto R = e.getPixelRect(); + // El marc l'alimenta el rect de col·lisió perquè coincideixi amb + // el que `entityAtPixel` considera "clicable". Així el dissenyador + // veu exactament la zona sensible al hover/selecció. + const auto R = e.getCollisionRect(); auto& screen = Screen::get(); const auto X = static_cast(R.x); const auto Y = static_cast(R.y); diff --git a/source/game/editor/editor.hpp b/source/game/editor/editor.hpp index 500db4f..e3ff0f4 100644 --- a/source/game/editor/editor.hpp +++ b/source/game/editor/editor.hpp @@ -52,6 +52,14 @@ class Editor { [[nodiscard]] auto active() const -> bool { return active_; } + // Cert quan, en mode editor, GameScene ha de pintar les entitats + // de la sala. Falses en mode TILE (volem veure el tilemap pelat + // per pintar-lo) i quan hi ha un picker obert (la caixa del picker + // ja tapa la pantalla i el soroll visual addicional sobra). + [[nodiscard]] auto shouldShowEntities() const -> bool { + return active_ && mode_ != Mode::TILE && picker_ == Picker::NONE; + } + // Processa input propi (brush, picker). No-op si no actiu. // `entities` cal perquè els modes ENTITY i HAND modifiquen el manager // en directe (col·loca/mou/borra entitats); en TILE no s'usa. diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index 61c9a8e..47550ad 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -1294,11 +1294,22 @@ void GameScene::render() { #else constexpr bool EDITOR_ACTIVE = false; #endif - if (EDITOR_ACTIVE) { - // Render barat de les entitats sense z-sort entre elles ni amb - // jugadors: ja no hi ha actors mòbils. L'ordre és el d'inserció - // del manager — suficient per editar. - entity_manager_.renderAll(); +#ifdef _DEBUG + const bool SHOW_ENTITIES_IN_EDITOR = editor_.shouldShowEntities(); +#else + constexpr bool SHOW_ENTITIES_IN_EDITOR = false; +#endif + if (SHOW_ENTITIES_IN_EDITOR) { + // Render de les entitats amb el mateix y-sort que el joc normal + // (sense jugadors): així una mòmia situada davant d'un sarc en + // falsa perspectiva queda dibuixada per damunt — i no a l'inrevés + // segons l'ordre del YAML. Reusem `EntityManager::renderEntity` + // (estàtic) i sortegem per `getFeetY()` ascendent. + auto sorted = entity_manager_.getEntities(); + std::ranges::stable_sort(sorted, [](const Entity* a, const Entity* b) { + return a->getFeetY() < b->getFeetY(); + }); + for (const auto* e : sorted) { EntityManager::renderEntity(*e); } } // Capa de pintat: va entre el tilemap base i els actors, perquè els // sarcòfags i el jugador la tapen. S'amaga al mode editor (decisió