226 lines
10 KiB
C++
226 lines
10 KiB
C++
#pragma once
|
||
|
||
#ifdef _DEBUG
|
||
|
||
#include <cstdint>
|
||
#include <memory>
|
||
#include <string>
|
||
#include <vector>
|
||
|
||
class Entity;
|
||
class EntityManager;
|
||
class Surface;
|
||
class Text;
|
||
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 (mode actiu, brush per mode, picker obert).
|
||
// - Input del ratolí i tecles internes (`T`, `E`, `H`, `Supr`, `ESC`).
|
||
// - Render del border blau + brush preview + picker.
|
||
// - Persistència del tilemap i de les entitats al YAML del repo en sortir.
|
||
//
|
||
// Modes del cursor (`Mode`):
|
||
// - TILE : el clic esquerre pinta el `brush_tile_` al tilemap.
|
||
// - ENTITY : el clic esquerre col·loca una instància de `brush_entity_`.
|
||
// - HAND : selecciona/mou/borra entitats existents.
|
||
//
|
||
// Pickers (`Picker`):
|
||
// - TILE_PICKER (`T`) : selecciona índex de tile.
|
||
// - ENTITY_PICKER (`E`) : selecciona tipus d'entitat (nom registrat al
|
||
// factory).
|
||
// Mútuament excloents: obrir un tanca l'altre.
|
||
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 i les entitats al YAML
|
||
// (writers surgicals) i notifica. Si la persistència falla, emet la
|
||
// notificació d'error.
|
||
void exit(Tilemap& tilemap, const EntityManager& entities);
|
||
|
||
// Helper: enter() si no actiu, exit() si actiu. Útil des de GameScene
|
||
// quan detecta la tecla de toggle.
|
||
void toggle(Tilemap& tilemap, const EntityManager& entities);
|
||
|
||
[[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.
|
||
void update(int delta_ms, Tilemap& tilemap, EntityManager& entities);
|
||
|
||
// Pinta el border blau i, segons mode i picker, brush preview, marc
|
||
// d'entitat seleccionada o el picker. No-op si no actiu.
|
||
// `entities` és necessari per al marc d'entitat hovered/seleccionada
|
||
// en Mode::HAND; en altres modes no s'usa.
|
||
void render(const Tilemap& tilemap, const EntityManager& entities) const;
|
||
|
||
private:
|
||
// Mode del cursor: determina què fa el clic esquerre sobre el mapa.
|
||
enum class Mode : std::uint8_t { TILE,
|
||
ENTITY,
|
||
HAND };
|
||
|
||
// Quin picker està obert (si n'hi ha). Mútuament excloents.
|
||
// - ENTITY_FRAME: sub-picker que apareix després de clicar una entitat
|
||
// amb variants al picker principal. Mostra tots els sub-sprites del
|
||
// atlas (índex frame) per a triar quina cara representa la instància.
|
||
enum class Picker : std::uint8_t { NONE,
|
||
TILE,
|
||
ENTITY,
|
||
ENTITY_FRAME };
|
||
|
||
// Consum de hotkeys pròpies (T, E, H, ESC). Modifica `mode_` / `picker_`.
|
||
// Es crida al principi d'`update` perquè altres handlers no vegin
|
||
// tecles ja interpretades pel propi editor.
|
||
void handleHotkeys();
|
||
|
||
// Aplica una tecla i retorna true si l'editor l'ha consumit. Aïllat
|
||
// per mantenir baixa la complexitat cognitiva de `handleHotkeys` —
|
||
// l'iteració del buffer queda neta i la lògica de transició viu en
|
||
// un sol lloc.
|
||
auto applyHotkey(int scancode) -> bool;
|
||
|
||
// Branques d'update segons (mode, picker). Només té sentit cridar-les
|
||
// des d'`update`.
|
||
void updateTileBrush(Tilemap& tilemap, int canvas_x, int canvas_y);
|
||
void updateEntityBrush(EntityManager& entities, int canvas_x, int canvas_y);
|
||
void updateHand(const Tilemap& tilemap, EntityManager& entities, int canvas_x, int canvas_y);
|
||
void updateTilePicker(const Tilemap& tilemap, int canvas_x, int canvas_y);
|
||
void updateEntityPicker(int canvas_x, int canvas_y);
|
||
void updateEntityFramePicker(int canvas_x, int canvas_y);
|
||
|
||
// Intent de col·locació segons brush_entity_ a la cel·la del cursor.
|
||
// Construeix un node spawn sintètic, fa el snap concret per tipus
|
||
// (sarc → rejilla 4×5; door → fila del mur i, de moment, columna
|
||
// canònica) i comprova solapaments. No-op si la cel·la està ocupada
|
||
// o el snap no és vàlid. Retorna true si s'ha col·locat.
|
||
auto tryPlaceEntity(EntityManager& entities) -> bool;
|
||
|
||
// Mou l'entitat seleccionada a la cel·la del cursor: borra l'antiga
|
||
// i crida `addSpawn` amb la nova posició. Reusa la mateixa lògica
|
||
// de snap/solapament que la col·locació nova, excloent l'entitat
|
||
// que es mou del check de solapament. Cas especial per a la porta:
|
||
// el sprite ocupa 3 cols (col-1, col, col+1) sobre el mur superior;
|
||
// per tant la columna central vàlida és [1, cols-2] — `tilemap` és
|
||
// necessari per saber `cols`.
|
||
auto tryMoveSelected(const Tilemap& tilemap, EntityManager& entities) const -> bool;
|
||
|
||
// Sub-handlers de `updateHand`. Aïllats per mantenir baixa la
|
||
// complexitat cognitiva del bucle d'esdeveniments.
|
||
void handleHandLeftClick(const Tilemap& tilemap, EntityManager& entities);
|
||
void handleHandRightClick();
|
||
|
||
// Borra l'entitat seleccionada. La validació de constraints
|
||
// (1 puerta, ≥1 sarc) es fa al pas 8.
|
||
void deleteSelected(EntityManager& entities);
|
||
|
||
// Render del marc d'una entitat: 1 px sobre el seu pixel_rect.
|
||
static void renderEntityFrame(const Entity& e, std::uint32_t argb);
|
||
|
||
// HUD d'edició: franja superior amb mode, brush, cursor i comptadors
|
||
// de constraints. Substitueix el HUD del joc mentre l'editor és actiu.
|
||
void renderEditorHud(const EntityManager& entities) const;
|
||
|
||
// Carrega la font 8bithud del cache. Lazy: al primer enter().
|
||
void ensureHudFont();
|
||
|
||
void renderTilePicker(const Tilemap& tilemap) const;
|
||
void renderEntityPicker(const Tilemap& tilemap) const;
|
||
void renderEntityFramePicker() const;
|
||
|
||
// Preview del sprite de l'entitat seleccionada com a brush, snapped al
|
||
// tile sota el cursor. Anchor "feet": base del sprite a la base inferior
|
||
// del tile, centrat horitzontalment — així stone cau exactament i column
|
||
// sobresurt cap amunt, coincidint amb el resultat de col·locar.
|
||
void renderEntityBrushPreview(const Tilemap& tilemap) const;
|
||
|
||
bool active_{false};
|
||
Mode mode_{Mode::TILE};
|
||
Picker picker_{Picker::NONE};
|
||
|
||
// Cursor en cel·les del tilemap. -1/-1 = fora del mapa.
|
||
int cursor_col_{-1};
|
||
int cursor_row_{-1};
|
||
|
||
// Brush actiu per Mode::TILE (índex al tileset; -1 = sense brush).
|
||
int brush_tile_{-1};
|
||
// Estat del TilePicker.
|
||
int tile_picker_scroll_{0};
|
||
int tile_picker_hover_{-1};
|
||
|
||
// Entrada editable del picker. Cada item recorda el nom (clau de
|
||
// factory + YAML), el surface del sprite (frame 0 com a icona) i la
|
||
// mida de cada frame al GIF — necessària per clipar el primer frame
|
||
// quan el GIF és un strip multi-frame.
|
||
// - `frame_count` és el nombre total de sub-sprites del atlas
|
||
// (cols × rows segons les dimensions del surface i del frame).
|
||
// - `has_variants` és `true` quan el template declara `editable_variants:
|
||
// true` i hi ha més d'un frame: aleshores clicar al picker obre el
|
||
// sub-picker de frames en lloc de fixar el brush directament.
|
||
struct EditableEntity {
|
||
std::string name;
|
||
std::shared_ptr<Surface> sprite;
|
||
int frame_w{0};
|
||
int frame_h{0};
|
||
int frame_count{1};
|
||
bool has_variants{false};
|
||
};
|
||
|
||
// Construïda lazy al primer enter() i preservada entre sessions:
|
||
// el cost és I/O + parse de N plantilles YAML, però `EntityLoader`
|
||
// ja les memoritza, així que rellegir només resoldria per sortida i
|
||
// entrada al editor — no val la pena.
|
||
void ensureEditableEntities();
|
||
|
||
std::vector<EditableEntity> editable_entities_;
|
||
|
||
// Brush actiu per Mode::ENTITY (nom registrat al factory; "" = cap).
|
||
std::string brush_entity_;
|
||
// Frame escollit per al brush actiu (-1 = no aplicable: entitat sense
|
||
// variants o brush encara per fixar). El emet el sub-picker en clicar
|
||
// un frame; `tryPlaceEntity` el passa al spawn YAML com a `frame: N`.
|
||
int brush_entity_frame_{-1};
|
||
// Estat de l'EntityPicker.
|
||
int entity_picker_scroll_{0};
|
||
int entity_picker_hover_{-1}; // Índex a la llista de noms.
|
||
// Estat del sub-picker de frames. `entity_frame_picker_source_` apunta
|
||
// a la entitat activa (vista al EntityPicker) per a la qual el
|
||
// sub-picker mostra les variants. Quan el sub-picker es tanca sense
|
||
// selecció, ho deixem a nullptr i `brush_entity_frame_ = -1`.
|
||
int entity_frame_picker_scroll_{0};
|
||
int entity_frame_picker_hover_{-1};
|
||
const EditableEntity* entity_frame_picker_source_{nullptr};
|
||
|
||
// Font compartida amb el Notifier/HUD: el surface el carrega Director
|
||
// a l'arrencada, ací només prenem la referència del cache. shared_ptr
|
||
// perquè `Text` accepta el surface per shared_ptr i així no copiem.
|
||
std::shared_ptr<Text> hud_font_;
|
||
|
||
// Selecció en Mode::HAND. Treballem amb índexs sobre el snapshot de
|
||
// `EntityManager::getEntities()` perquè és el que ens dóna l'API
|
||
// pública. Després de cada mutació (move/delete) ho recalculem; com
|
||
// que l'editor congela el joc, el snapshot només canvia per accions
|
||
// pròpies.
|
||
int hand_hover_idx_{-1};
|
||
int hand_selected_idx_{-1};
|
||
};
|
||
|
||
#endif // _DEBUG
|