feat(editor): mode editor mínim amb toggle, border blau i mutex amb pausa
This commit is contained in:
1 parent
329bf692e6
commit
a612a8255f
9 files changed
+232
-30
No files matched your search
@@ -93,6 +93,10 @@ set(APP_SOURCES
|
|||||||
source/game/map/map.cpp
|
source/game/map/map.cpp
|
||||||
source/game/map/tilemap.cpp
|
source/game/map/tilemap.cpp
|
||||||
source/game/map/tilemap_writer.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}
|
${ENTITY_SOURCES}
|
||||||
# Fitxer només del binari principal: `loadFromLevel` depèn de
|
# Fitxer només del binari principal: `loadFromLevel` depèn de
|
||||||
# Resource::Cache (→ Surface → GIF → àudio). Separat per no arrossegar
|
# Resource::Cache (→ Surface → GIF → àudio). Separat per no arrossegar
|
||||||
|
|||||||
@@ -41,6 +41,12 @@ keys:
|
|||||||
- id: pause
|
- id: pause
|
||||||
code: "F11"
|
code: "F11"
|
||||||
desc: "Pausa el joc (o botó START del mando)"
|
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
|
- id: exit
|
||||||
code: "Escape"
|
code: "Escape"
|
||||||
desc: "Eixir (confirmar amb segona pulsació)"
|
desc: "Eixir (confirmar amb segona pulsació)"
|
||||||
@@ -23,6 +23,9 @@ input:
|
|||||||
scaling_integer: "Escalat enter"
|
scaling_integer: "Escalat enter"
|
||||||
press_again_exit: "Prem una altra volta per eixir"
|
press_again_exit: "Prem una altra volta per eixir"
|
||||||
paused: "Pausa"
|
paused: "Pausa"
|
||||||
|
editor_on: "Editor ON"
|
||||||
|
editor_off: "Editor OFF"
|
||||||
|
editor_save_error: "Editor: error guardant el mapa"
|
||||||
|
|
||||||
menu:
|
menu:
|
||||||
titles:
|
titles:
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ input:
|
|||||||
scaling_integer: "Scaling integer"
|
scaling_integer: "Scaling integer"
|
||||||
press_again_exit: "Press again to exit"
|
press_again_exit: "Press again to exit"
|
||||||
paused: "Paused"
|
paused: "Paused"
|
||||||
|
editor_on: "Editor ON"
|
||||||
|
editor_off: "Editor OFF"
|
||||||
|
editor_save_error: "Editor: map save failed"
|
||||||
|
|
||||||
menu:
|
menu:
|
||||||
titles:
|
titles:
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
#include "game/editor/editor.hpp"
|
||||||
|
|
||||||
|
#ifdef _DEBUG
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#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
|
||||||
@@ -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
|
||||||
@@ -127,6 +127,10 @@ GameScene::GameScene(Config config)
|
|||||||
GameScene::~GameScene() = default;
|
GameScene::~GameScene() = default;
|
||||||
|
|
||||||
void GameScene::handlePauseInput() {
|
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");
|
const SDL_Scancode PAUSE_SC = KeyConfig::get().scancode("pause");
|
||||||
SDL_Gamepad* pad = GamepadManager::get().getPrimary();
|
SDL_Gamepad* pad = GamepadManager::get().getPrimary();
|
||||||
for (const auto& event : EventBuffer::events) {
|
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) {
|
void GameScene::update(int delta_ms) {
|
||||||
handlePauseInput();
|
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);
|
GlobalInputs::update(delta_ms);
|
||||||
if (GlobalInputs::wantsQuit()) { wants_quit_ = true; }
|
if (GlobalInputs::wantsQuit()) { wants_quit_ = true; }
|
||||||
|
|
||||||
// El món es congela durant la pausa: ni jugadors ni entitats avancen,
|
// El món es congela durant la pausa i durant el mode editor: ni
|
||||||
// ni es resolen col·lisions. Notifier i RenderInfo sí continuen perquè
|
// jugadors ni entitats avancen, ni es resolen col·lisions. Notifier i
|
||||||
// els usa la UI de sistema (notificacions d'hotkeys, overlay de debug).
|
// RenderInfo sí continuen perquè els usa la UI de sistema.
|
||||||
if (!paused_) {
|
#ifdef _DEBUG
|
||||||
|
const bool WORLD_FROZEN = paused_ || editor_.active();
|
||||||
|
#else
|
||||||
|
const bool WORLD_FROZEN = paused_;
|
||||||
|
#endif
|
||||||
|
if (!WORLD_FROZEN) {
|
||||||
const float DT = static_cast<float>(delta_ms) / 1000.0F;
|
const float DT = static_cast<float>(delta_ms) / 1000.0F;
|
||||||
for (auto& player : players_) { player->update(DT); }
|
for (auto& player : players_) { player->update(DT); }
|
||||||
// El tick d'entitats DESPRÉS del tick dels jugadors: si el jugador ha
|
// 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);
|
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));
|
RenderInfo::get().update(delta_ms, Options::buildRenderInfoFrame(UiColor::RENDER_INFO_TEXT, UiColor::RENDER_INFO_SHADOW));
|
||||||
Notifier::get().update(delta_ms);
|
Notifier::get().update(delta_ms);
|
||||||
}
|
}
|
||||||
@@ -186,35 +220,49 @@ void GameScene::render() {
|
|||||||
// col·lisions del jugador.
|
// col·lisions del jugador.
|
||||||
tilemap_.render();
|
tilemap_.render();
|
||||||
|
|
||||||
// Z-order interleaving entitats ↔ players per `getFeetY()` ascendent:
|
#ifdef _DEBUG
|
||||||
// els actors amb els peus més amunt queden dibuixats primer (al fons),
|
// En mode editor amaguem entitats i jugador: el tilemap queda net
|
||||||
// els més avall al davant. Així un sarc situat per darrere d'un player
|
// per a poder clicar cel·la per cel·la sense interferència visual.
|
||||||
// queda tapat per ell, i un sarc a davant el tapa a ell.
|
// El HUD futur (quan s'afegeixi) també farà cas d'aquest flag.
|
||||||
struct Renderable {
|
const bool SKIP_ACTORS = editor_.active();
|
||||||
float feet_y;
|
#else
|
||||||
Entity* entity; // nullable; si és nullptr, s'usa `player`.
|
constexpr bool SKIP_ACTORS = false;
|
||||||
Player* player; // nullable; si és nullptr, s'usa `entity`.
|
#endif
|
||||||
};
|
if (!SKIP_ACTORS) {
|
||||||
std::vector<Renderable> order;
|
// Z-order interleaving entitats ↔ players per `getFeetY()` ascendent:
|
||||||
order.reserve(entity_manager_.entityCount() + players_.size());
|
// els actors amb els peus més amunt queden dibuixats primer (al fons),
|
||||||
const auto ENTITIES = entity_manager_.getEntities();
|
// els més avall al davant. Així un sarc situat per darrere d'un player
|
||||||
std::ranges::transform(ENTITIES, std::back_inserter(order), [](Entity* e) {
|
// queda tapat per ell, i un sarc a davant el tapa a ell.
|
||||||
return Renderable{.feet_y = e->getFeetY(), .entity = e, .player = nullptr};
|
struct Renderable {
|
||||||
});
|
float feet_y;
|
||||||
std::ranges::transform(players_, std::back_inserter(order), [](const auto& p) {
|
Entity* entity; // nullable; si és nullptr, s'usa `player`.
|
||||||
return Renderable{.feet_y = p->getFeetY(), .entity = nullptr, .player = p.get()};
|
Player* player; // nullable; si és nullptr, s'usa `entity`.
|
||||||
});
|
};
|
||||||
std::ranges::stable_sort(order, [](const Renderable& a, const Renderable& b) {
|
std::vector<Renderable> order;
|
||||||
return a.feet_y < b.feet_y;
|
order.reserve(entity_manager_.entityCount() + players_.size());
|
||||||
});
|
const auto ENTITIES = entity_manager_.getEntities();
|
||||||
for (const auto& r : order) {
|
std::ranges::transform(ENTITIES, std::back_inserter(order), [](Entity* e) {
|
||||||
if (r.entity != nullptr) {
|
return Renderable{.feet_y = e->getFeetY(), .entity = e, .player = nullptr};
|
||||||
EntityManager::renderEntity(*r.entity);
|
});
|
||||||
} else if (r.player != nullptr) {
|
std::ranges::transform(players_, std::back_inserter(order), [](const auto& p) {
|
||||||
r.player->render();
|
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)
|
// 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)
|
// 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
|
// i el cos en color de notificació, prou contrastat amb el fons del
|
||||||
|
|||||||
@@ -8,6 +8,10 @@
|
|||||||
#include "game/entities/entity_manager.hpp"
|
#include "game/entities/entity_manager.hpp"
|
||||||
#include "game/map/tilemap.hpp"
|
#include "game/map/tilemap.hpp"
|
||||||
|
|
||||||
|
#ifdef _DEBUG
|
||||||
|
#include "game/editor/editor.hpp"
|
||||||
|
#endif
|
||||||
|
|
||||||
class Player;
|
class Player;
|
||||||
class Text;
|
class Text;
|
||||||
|
|
||||||
@@ -56,4 +60,12 @@ class GameScene : public Scene {
|
|||||||
// (cap altra escena l'allibera, així que l'accés és segur).
|
// (cap altra escena l'allibera, així que l'accés és segur).
|
||||||
std::shared_ptr<Text> pause_font_;
|
std::shared_ptr<Text> pause_font_;
|
||||||
bool paused_{false};
|
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
|
||||||
};
|
};
|
||||||
@@ -156,6 +156,17 @@ namespace Defaults {
|
|||||||
constexpr Uint8 MENU_DISABLED = 34; // Label en gris quan no aplica.
|
constexpr Uint8 MENU_DISABLED = 34; // Label en gris quan no aplica.
|
||||||
} // namespace UI
|
} // 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
|
// Rutes d'assets per piràmide. Centralitzats ací perquè `GameScene` no
|
||||||
// en contingui cap literal; afegir una piràmide nova serà un altre
|
// en contingui cap literal; afegir una piràmide nova serà un altre
|
||||||
// conjunt de constants + un Config distint passat al ctor.
|
// conjunt de constants + un Config distint passat al ctor.
|
||||||
|
|||||||
Reference in new issue
Block a user