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
@@ -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]
|
||||||
@@ -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();
|
||||||
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;
|
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);
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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ó
|
||||||
|
|||||||
Reference in new issue
Block a user