feat(editor): hand pick per base, z-sort, amaga entitats en TILE, HUD 2-files centrat

This commit is contained in:
JailDesigner committed 2026-05-02 12:42:47 +02:00
1 parent c0362abf67
commit a254fdd669
4 files changed
+92 -33

No files matched your search

+3 -3
View File
@@ -133,8 +133,6 @@ entities:
- type: door - type: door
- type: mummy - type: mummy
position: [10, 7] position: [10, 7]
- type: mummy
position: [7, 9]
- type: hole - type: hole
position: [4, 7] position: [4, 7]
- type: hole - type: hole
@@ -144,4 +142,6 @@ entities:
- type: hole - type: hole
position: [15, 5] position: [15, 5]
- type: hole - type: hole
position: [6, 5] position: [8, 5]
- type: mummy
position: [8, 9]
+65 -25
View File
@@ -6,11 +6,11 @@
#include <algorithm> #include <algorithm>
#include <filesystem> #include <filesystem>
#include <numeric>
#include <sstream> #include <sstream>
#include "core/input/key_config.hpp" #include "core/input/key_config.hpp"
#include "core/locale/locale.hpp" #include "core/locale/locale.hpp"
#include "core/rendering/hud_constants.hpp"
#include "core/rendering/notification_presets.hpp" #include "core/rendering/notification_presets.hpp"
#include "core/rendering/screen.hpp" #include "core/rendering/screen.hpp"
#include "core/rendering/surface.hpp" #include "core/rendering/surface.hpp"
@@ -180,14 +180,17 @@ namespace {
void Editor::enter() { void Editor::enter() {
if (active_) { return; } if (active_) { return; }
active_ = true; active_ = true;
// Estat de novella: tornem a Mode::TILE sense brush ni picker. Els // Estat de novella: comencem en mode HAND (selecció/manipulació
// brushs (tile/entity) i la selecció de la mà no es preserven entre // d'entitats existents) — el cas d'ús més habitual quan obres
// sessions del editor: cada entrada parteix net. // l'editor és inspeccionar/moure/borrar el que ja hi ha. Els
mode_ = Mode::TILE; // brushs i la selecció no es preserven entre sessions: cada
// entrada parteix net.
mode_ = Mode::HAND;
picker_ = Picker::NONE; picker_ = Picker::NONE;
brush_tile_ = -1; brush_tile_ = -1;
brush_entity_.clear(); brush_entity_.clear();
hand_selected_idx_ = -1; hand_selected_idx_ = -1;
hand_hover_idx_ = -1;
ensureEditableEntities(); ensureEditableEntities();
ensureHudFont(); ensureHudFont();
Notification::notify(Locale::get().text("input.editor_on")); Notification::notify(Locale::get().text("input.editor_on"));
@@ -544,15 +547,20 @@ void Editor::updateEntityBrush(EntityManager& entities, int /*canvas_x*/, int /*
namespace { namespace {
// Index de l'entitat sota el píxel donat. -1 si cap. Usem el rect de // Index de l'entitat sota el píxel donat. -1 si cap. Usem el rect
// pintat (`getPixelRect`) i no el de col·lisió perquè el dissenyador // de col·lisió (caixa base alineada al tilemap) i NO el sprite
// espera "fer clic on es veu", encara que el cos físic sigui més // sencer: a entitats amb falsa perspectiva (mòmia 16×32, sarc
// estret (típic en sarc/porta amb falsa perspectiva). // 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<Entity*>& list, int px, int py) -> int { auto entityAtPixel(const std::vector<Entity*>& list, int px, int py) -> int {
const auto FX = static_cast<float>(px); const auto FX = static_cast<float>(px);
const auto FY = static_cast<float>(py); const auto FY = static_cast<float>(py);
for (int i = static_cast<int>(list.size()) - 1; i >= 0; --i) { for (int i = static_cast<int>(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; } if (FX >= R.x && FX < R.x + R.w && FY >= R.y && FY < R.y + R.h) { return i; }
} }
return -1; return -1;
@@ -662,8 +670,13 @@ void Editor::deleteSelected(EntityManager& entities) {
void Editor::renderEditorHud(const EntityManager& entities) const { void Editor::renderEditorHud(const EntityManager& entities) const {
if (hud_font_ == nullptr || Map::hud_h <= 0) { return; } if (hud_font_ == nullptr || Map::hud_h <= 0) { return; }
auto& screen = Screen::get(); auto& screen = Screen::get();
// 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"); const auto& pal = Resource::Cache::get().getPalette("ui");
screen.fillRect(0, 0, GameCanvas::WIDTH, Map::hud_h, pal[UiConfig::get().hudBg()]); screen.fillRect(0, 0, GameCanvas::WIDTH, Map::hud_h, pal[UiConfig::get().hudBg()]);
}
std::string mode_str; std::string mode_str;
switch (mode_) { switch (mode_) {
@@ -700,28 +713,55 @@ void Editor::renderEditorHud(const EntityManager& entities) const {
const auto LBL = UiConfig::get().hudLabel(); const auto LBL = UiConfig::get().hudLabel();
const auto VAL = UiConfig::get().hudValue(); const auto VAL = UiConfig::get().hudValue();
const auto FLASH = UiConfig::get().hudFlash();
const int KERN = UiConfig::get().hudFont().kerning; const int KERN = UiConfig::get().hudFont().kerning;
const int Y = (Map::hud_h - Rendering::Hud::FONT_SIZE_PX) / 2; // Mateixos Y que el HUD del joc per simetria visual: línia 1 = 4,
int x = 2; // 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) { struct Segment {
hud_font_->writeColored(x, Y, s, color, KERN); std::string text;
x += hud_font_->length(s, KERN); std::uint8_t color;
};
auto draw_line = [&](const std::vector<Segment>& 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); // Línia 1: estat actiu (mode + brush/selecció + cursor).
draw(" " + mode_str, VAL); draw_line({{.text = "MODE:", .color = LBL},
draw(" " + brush_label, LBL); {.text = " " + mode_str, .color = VAL},
draw(" " + brush_value, VAL); {.text = " " + brush_label, .color = LBL},
draw(" " + COORD, VAL); {.text = " " + brush_value, .color = VAL},
draw(" DOOR:", LBL); {.text = " " + COORD, .color = VAL}},
draw(" " + std::to_string(countByName(entities, "door")), VAL); LINE1_Y);
draw(" SARCS:", LBL);
draw(" " + std::to_string(countByName(entities, "sarcophagus")), VAL); // 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) { 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(); auto& screen = Screen::get();
const auto X = static_cast<int>(R.x); const auto X = static_cast<int>(R.x);
const auto Y = static_cast<int>(R.y); const auto Y = static_cast<int>(R.y);
+8
View File
@@ -52,6 +52,14 @@ class Editor {
[[nodiscard]] auto active() const -> bool { return active_; } [[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. // Processa input propi (brush, picker). No-op si no actiu.
// `entities` cal perquè els modes ENTITY i HAND modifiquen el manager // `entities` cal perquè els modes ENTITY i HAND modifiquen el manager
// en directe (col·loca/mou/borra entitats); en TILE no s'usa. // en directe (col·loca/mou/borra entitats); en TILE no s'usa.
+16 -5
View File
@@ -1294,11 +1294,22 @@ void GameScene::render() {
#else #else
constexpr bool EDITOR_ACTIVE = false; constexpr bool EDITOR_ACTIVE = false;
#endif #endif
if (EDITOR_ACTIVE) { #ifdef _DEBUG
// Render barat de les entitats sense z-sort entre elles ni amb const bool SHOW_ENTITIES_IN_EDITOR = editor_.shouldShowEntities();
// jugadors: ja no hi ha actors mòbils. L'ordre és el d'inserció #else
// del manager — suficient per editar. constexpr bool SHOW_ENTITIES_IN_EDITOR = false;
entity_manager_.renderAll(); #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 // 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ó // sarcòfags i el jugador la tapen. S'amaga al mode editor (decisió