feat(editor): hand pick per base, z-sort, amaga entitats en TILE, HUD 2-files centrat
This commit is contained in:
1 parent
c0362abf67
commit
a254fdd669
4 files changed
+94
-35
No files matched your search
@@ -6,11 +6,11 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <numeric>
|
||||
#include <sstream>
|
||||
|
||||
#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<Entity*>& list, int px, int py) -> int {
|
||||
const auto FX = static_cast<float>(px);
|
||||
const auto FY = static_cast<float>(py);
|
||||
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; }
|
||||
}
|
||||
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<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);
|
||||
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<int>(R.x);
|
||||
const auto Y = static_cast<int>(R.y);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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ó
|
||||
|
||||
Reference in new issue
Block a user