From a612a8255f348d5fce352dd0ffaba0baabf9ae6a Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Thu, 23 Apr 2026 12:09:04 +0200 Subject: [PATCH] =?UTF-8?q?feat(editor):=20mode=20editor=20m=C3=ADnim=20am?= =?UTF-8?q?b=20toggle,=20border=20blau=20i=20mutex=20amb=20pausa?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CMakeLists.txt | 4 ++ data/input/keys.yaml | 6 ++ data/locale/ca.yaml | 3 + data/locale/en.yaml | 3 + source/game/editor/editor.cpp | 59 ++++++++++++++++ source/game/editor/editor.hpp | 56 ++++++++++++++++ source/game/scenes/game_scene.cpp | 108 +++++++++++++++++++++--------- source/game/scenes/game_scene.hpp | 12 ++++ source/utils/defaults.hpp | 11 +++ 9 files changed, 232 insertions(+), 30 deletions(-) create mode 100644 source/game/editor/editor.cpp create mode 100644 source/game/editor/editor.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 90adcd8..8d0b61f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -93,6 +93,10 @@ set(APP_SOURCES source/game/map/map.cpp source/game/map/tilemap.cpp source/game/map/tilemap_writer.cpp + # Mòdul editor (només activa contingut en Debug via `#ifdef _DEBUG`). + # En Release el fitxer compila a un object buit i l'enllaçador no + # arrossega cap símbol del joc final. + source/game/editor/editor.cpp ${ENTITY_SOURCES} # Fitxer només del binari principal: `loadFromLevel` depèn de # Resource::Cache (→ Surface → GIF → àudio). Separat per no arrossegar diff --git a/data/input/keys.yaml b/data/input/keys.yaml index b9ddc4a..f8782a2 100644 --- a/data/input/keys.yaml +++ b/data/input/keys.yaml @@ -41,6 +41,12 @@ keys: - id: pause code: "F11" desc: "Pausa el joc (o botó START del mando)" + - id: editor_toggle + code: "9" + desc: "Entra/surt del mode editor (només Debug)" + - id: editor_picker + code: "T" + desc: "Obri/tanca el tilepicker dins de l'editor" - id: exit code: "Escape" desc: "Eixir (confirmar amb segona pulsació)" diff --git a/data/locale/ca.yaml b/data/locale/ca.yaml index 25cf6e8..d330f24 100644 --- a/data/locale/ca.yaml +++ b/data/locale/ca.yaml @@ -23,6 +23,9 @@ input: scaling_integer: "Escalat enter" press_again_exit: "Prem una altra volta per eixir" paused: "Pausa" + editor_on: "Editor ON" + editor_off: "Editor OFF" + editor_save_error: "Editor: error guardant el mapa" menu: titles: diff --git a/data/locale/en.yaml b/data/locale/en.yaml index be9517b..590ee33 100644 --- a/data/locale/en.yaml +++ b/data/locale/en.yaml @@ -23,6 +23,9 @@ input: scaling_integer: "Scaling integer" press_again_exit: "Press again to exit" paused: "Paused" + editor_on: "Editor ON" + editor_off: "Editor OFF" + editor_save_error: "Editor: map save failed" menu: titles: diff --git a/source/game/editor/editor.cpp b/source/game/editor/editor.cpp new file mode 100644 index 0000000..8552a64 --- /dev/null +++ b/source/game/editor/editor.cpp @@ -0,0 +1,59 @@ +#include "game/editor/editor.hpp" + +#ifdef _DEBUG + +#include + +#include "core/locale/locale.hpp" +#include "core/rendering/notification_presets.hpp" +#include "core/rendering/screen.hpp" +#include "game/map/tilemap.hpp" +#include "utils/defaults.hpp" +#include "utils/defines.hpp" + +void Editor::enter() { + if (active_) { return; } + active_ = true; + Notification::notify(Locale::get().text("input.editor_on")); +} + +// cppcheck-suppress constParameterReference +// Tilemap& mutable per simetria amb `toggle()` i per futures +// invalidacions de caches d'edició, encara que `save()` sigui const. +void Editor::exit(Tilemap& tilemap) { + if (!active_) { return; } + active_ = false; + const bool SAVED = tilemap.save(Defaults::Editor::SAVE_PATH); + Notification::notify(Locale::get().text(SAVED ? "input.editor_off" : "input.editor_save_error")); +} + +void Editor::toggle(Tilemap& tilemap) { + if (active_) { + exit(tilemap); + } else { + enter(); + } +} + +// cppcheck-suppress constParameterReference +// Tilemap& mutable: al pas següent `setTileAt()` (no const) el muta. +void Editor::update(int /*delta_ms*/, Tilemap& tilemap) { + if (!active_) { return; } + cursor_col_ = -1; + cursor_row_ = -1; + (void)tilemap; // TODO: brush + click + picker al pas següent. +} + +void Editor::render(const Tilemap& /*tilemap*/) const { + if (!active_) { return; } + auto& screen = Screen::get(); + constexpr int W = GameCanvas::WIDTH; + constexpr int H = GameCanvas::HEIGHT; + constexpr auto ARGB = Defaults::Editor::BORDER_ARGB; + screen.fillRect(0, 0, W, 1, ARGB); // top + screen.fillRect(0, H - 1, W, 1, ARGB); // bottom + screen.fillRect(0, 0, 1, H, ARGB); // left + screen.fillRect(W - 1, 0, 1, H, ARGB); // right +} + +#endif // _DEBUG diff --git a/source/game/editor/editor.hpp b/source/game/editor/editor.hpp new file mode 100644 index 0000000..3e442f0 --- /dev/null +++ b/source/game/editor/editor.hpp @@ -0,0 +1,56 @@ +#pragma once + +#ifdef _DEBUG + +class Tilemap; + +// Mode editor modal del GameScene. Només disponible en Debug — en Release +// aquesta classe no existeix (el .cpp es compila a un object buit i cap +// codi del joc final en fa referència). +// +// L'editor NO gestiona la tecla de toggle pròpia (`editor_toggle`): això +// ho fa GameScene per poder decidir la coexistència amb la pausa F11 +// (mútuament excloents). L'editor s'encarrega de: +// - Estat propi (actiu/no actiu, brush, picker obert). +// - Input del ratolí i tecles internes (`T`, `ESC`). +// - Render del border blau + brush preview + picker. +// - Persistència del tilemap al YAML del repo en sortir. +// +// GameScene, quan `active()` és true, salta el tick de jugadors/entitats +// i no els pinta; el tilemap sí es pinta perquè és el que s'està editant. +class Editor { + public: + // Entra al mode editor: notifica i marca `active=true`. La congelació + // d'entitats / jugador la gestiona GameScene consultant `active()`. + void enter(); + + // Surt del mode editor: persisteix el tilemap al YAML (writer surgical) + // i notifica. Si la persistència falla, emet la notificació d'error. + void exit(Tilemap& tilemap); + + // Helper: enter() si no actiu, exit() si actiu. Útil des de GameScene + // quan detecta la tecla de toggle. + void toggle(Tilemap& tilemap); + + [[nodiscard]] auto active() const -> bool { return active_; } + + // Processa input propi (brush, picker). No-op si no actiu. + void update(int delta_ms, Tilemap& tilemap); + + // Pinta el border blau i, més endavant, brush preview + picker. + // No-op si no actiu. + void render(const Tilemap& tilemap) const; + + private: + bool active_{false}; + + // Estat de l'edició (brush + cursor + picker). Es poblen al pas + // següent; els declarem ara perquè `update()` pugui mutar-los i les + // signatures queden estables entre hitos. + int brush_{-1}; // Índex de tile al tileset; -1 = sense brush. + int cursor_col_{-1}; // Cel·la sota el cursor (-1 = fora). + int cursor_row_{-1}; + bool picker_open_{false}; +}; + +#endif // _DEBUG diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index ec7d561..cbf252c 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -127,6 +127,10 @@ GameScene::GameScene(Config config) GameScene::~GameScene() = default; void GameScene::handlePauseInput() { +#ifdef _DEBUG + // Mutex amb l'editor: si està actiu, ignorem la tecla de pausa. + if (editor_.active()) { return; } +#endif const SDL_Scancode PAUSE_SC = KeyConfig::get().scancode("pause"); SDL_Gamepad* pad = GamepadManager::get().getPrimary(); for (const auto& event : EventBuffer::events) { @@ -142,16 +146,42 @@ void GameScene::handlePauseInput() { } } +#ifdef _DEBUG +namespace { + auto detectEditorToggle() -> bool { + const SDL_Scancode SC = KeyConfig::get().scancode("editor_toggle"); + if (SC == SDL_SCANCODE_UNKNOWN) { return false; } + return std::ranges::any_of(EventBuffer::events, [SC](const SDL_Event& ev) { + return ev.type == SDL_EVENT_KEY_DOWN && ev.key.scancode == SC; + }); + } +} // namespace +#endif + void GameScene::update(int delta_ms) { handlePauseInput(); +#ifdef _DEBUG + // Toggle de l'editor: només si la pausa no està activa (mútuament + // excloents). L'editor mateix no consumeix la tecla — la llegim ací + // per decidir la coexistència. + if (!paused_ && detectEditorToggle()) { + editor_.toggle(tilemap_); + } +#endif + GlobalInputs::update(delta_ms); if (GlobalInputs::wantsQuit()) { wants_quit_ = true; } - // El món es congela durant la pausa: ni jugadors ni entitats avancen, - // ni es resolen col·lisions. Notifier i RenderInfo sí continuen perquè - // els usa la UI de sistema (notificacions d'hotkeys, overlay de debug). - if (!paused_) { + // El món es congela durant la pausa i durant el mode editor: ni + // jugadors ni entitats avancen, ni es resolen col·lisions. Notifier i + // RenderInfo sí continuen perquè els usa la UI de sistema. +#ifdef _DEBUG + const bool WORLD_FROZEN = paused_ || editor_.active(); +#else + const bool WORLD_FROZEN = paused_; +#endif + if (!WORLD_FROZEN) { const float DT = static_cast(delta_ms) / 1000.0F; for (auto& player : players_) { player->update(DT); } // El tick d'entitats DESPRÉS del tick dels jugadors: si el jugador ha @@ -172,6 +202,10 @@ void GameScene::update(int delta_ms) { entity_manager_.checkPlayerCollisions(player_ptrs); } +#ifdef _DEBUG + editor_.update(delta_ms, tilemap_); +#endif + RenderInfo::get().update(delta_ms, Options::buildRenderInfoFrame(UiColor::RENDER_INFO_TEXT, UiColor::RENDER_INFO_SHADOW)); Notifier::get().update(delta_ms); } @@ -186,35 +220,49 @@ void GameScene::render() { // col·lisions del jugador. tilemap_.render(); - // Z-order interleaving entitats ↔ players per `getFeetY()` ascendent: - // els actors amb els peus més amunt queden dibuixats primer (al fons), - // els més avall al davant. Així un sarc situat per darrere d'un player - // queda tapat per ell, i un sarc a davant el tapa a ell. - struct Renderable { - float feet_y; - Entity* entity; // nullable; si és nullptr, s'usa `player`. - Player* player; // nullable; si és nullptr, s'usa `entity`. - }; - std::vector order; - order.reserve(entity_manager_.entityCount() + players_.size()); - const auto ENTITIES = entity_manager_.getEntities(); - std::ranges::transform(ENTITIES, std::back_inserter(order), [](Entity* e) { - return Renderable{.feet_y = e->getFeetY(), .entity = e, .player = nullptr}; - }); - std::ranges::transform(players_, std::back_inserter(order), [](const auto& p) { - return Renderable{.feet_y = p->getFeetY(), .entity = nullptr, .player = p.get()}; - }); - std::ranges::stable_sort(order, [](const Renderable& a, const Renderable& b) { - return a.feet_y < b.feet_y; - }); - for (const auto& r : order) { - if (r.entity != nullptr) { - EntityManager::renderEntity(*r.entity); - } else if (r.player != nullptr) { - r.player->render(); +#ifdef _DEBUG + // En mode editor amaguem entitats i jugador: el tilemap queda net + // per a poder clicar cel·la per cel·la sense interferència visual. + // El HUD futur (quan s'afegeixi) també farà cas d'aquest flag. + const bool SKIP_ACTORS = editor_.active(); +#else + constexpr bool SKIP_ACTORS = false; +#endif + if (!SKIP_ACTORS) { + // Z-order interleaving entitats ↔ players per `getFeetY()` ascendent: + // els actors amb els peus més amunt queden dibuixats primer (al fons), + // els més avall al davant. Així un sarc situat per darrere d'un player + // queda tapat per ell, i un sarc a davant el tapa a ell. + struct Renderable { + float feet_y; + Entity* entity; // nullable; si és nullptr, s'usa `player`. + Player* player; // nullable; si és nullptr, s'usa `entity`. + }; + std::vector order; + order.reserve(entity_manager_.entityCount() + players_.size()); + const auto ENTITIES = entity_manager_.getEntities(); + std::ranges::transform(ENTITIES, std::back_inserter(order), [](Entity* e) { + return Renderable{.feet_y = e->getFeetY(), .entity = e, .player = nullptr}; + }); + std::ranges::transform(players_, std::back_inserter(order), [](const auto& p) { + return Renderable{.feet_y = p->getFeetY(), .entity = nullptr, .player = p.get()}; + }); + std::ranges::stable_sort(order, [](const Renderable& a, const Renderable& b) { + return a.feet_y < b.feet_y; + }); + for (const auto& r : order) { + if (r.entity != nullptr) { + EntityManager::renderEntity(*r.entity); + } else if (r.player != nullptr) { + r.player->render(); + } } } +#ifdef _DEBUG + editor_.render(tilemap_); +#endif + // Overlay de pausa: text centrat amb stroke de 8 veïns (STROKE_FLAG) // que fa de contorn. Fem el contorn blanc (índex 255 de sega-cube.pal) // i el cos en color de notificació, prou contrastat amb el fons del diff --git a/source/game/scenes/game_scene.hpp b/source/game/scenes/game_scene.hpp index 2d9e951..f4fa9d1 100644 --- a/source/game/scenes/game_scene.hpp +++ b/source/game/scenes/game_scene.hpp @@ -8,6 +8,10 @@ #include "game/entities/entity_manager.hpp" #include "game/map/tilemap.hpp" +#ifdef _DEBUG +#include "game/editor/editor.hpp" +#endif + class Player; class Text; @@ -56,4 +60,12 @@ class GameScene : public Scene { // (cap altra escena l'allibera, així que l'accés és segur). std::shared_ptr pause_font_; bool paused_{false}; + +#ifdef _DEBUG + // Editor modal del tilemap. Mútuament excloent amb la pausa: si `paused_` + // la tecla de toggle de l'editor s'ignora, i viceversa. Quan + // `editor_.active()` és true, saltem entitats i jugador (update i + // render) i deixem el tilemap visible perquè es pugui editar. + Editor editor_; +#endif }; diff --git a/source/utils/defaults.hpp b/source/utils/defaults.hpp index e88e93d..0ea9db0 100644 --- a/source/utils/defaults.hpp +++ b/source/utils/defaults.hpp @@ -156,6 +156,17 @@ namespace Defaults { constexpr Uint8 MENU_DISABLED = 34; // Label en gris quan no aplica. } // namespace UI +#ifdef _DEBUG + // Constants de l'editor de tilemap. Només presents en Debug — en + // Release l'editor no existeix, així que ni les constants tenen sentit. + namespace Editor { + // Color del rectangle 1 px que envolta el canvas durant l'edició. + constexpr std::uint32_t BORDER_ARGB = 0xFF2244FFU; + // Ruta de persistència: writer surgical sobre la font del repo. + constexpr const char* SAVE_PATH = "data/maps/pyramid.yaml"; + } // namespace Editor +#endif + // Rutes d'assets per piràmide. Centralitzats ací perquè `GameScene` no // en contingui cap literal; afegir una piràmide nova serà un altre // conjunt de constants + un Config distint passat al ctor.