Files
aee-arcade/source/game/editor/editor.cpp
T

257 lines
9.7 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "game/editor/editor.hpp"
#ifdef _DEBUG
#include <SDL3/SDL.h>
#include <algorithm>
#include "core/input/key_config.hpp"
#include "core/locale/locale.hpp"
#include "core/rendering/notification_presets.hpp"
#include "core/rendering/screen.hpp"
#include "core/rendering/surface.hpp"
#include "core/resources/resource_cache.hpp"
#include "core/resources/resource_helper.hpp"
#include "core/system/event_buffer.hpp"
#include "game/levels/level_manager.hpp"
#include "game/map/tilemap.hpp"
#include "utils/defaults.hpp"
#include "utils/defines.hpp"
namespace {
// Geometria del TilePicker en espai canvas. `cols × rows_visible` tiles
// encapsulats dins d'un rectangle amb `PICKER_MARGIN` per vora. Si el
// tileset cap en amplada (TCOLS·tile_size ≤ INNER_W), en respectem la
// graella natural — els tiles surten ordenats igual que al GIF, només
// amb scroll vertical si cal. Si no, linealitzem els índexs en una
// graella més ampla que sí cap, amb scroll vertical per veure la resta.
struct PickerLayout {
int x{0};
int y{0};
int w{0};
int h{0};
int cols{0};
int rows_visible{0};
int tile_size{0};
int total_tiles{0};
int max_scroll{0};
};
auto layoutPicker(const Tilemap& tm) -> PickerLayout {
PickerLayout lay{};
lay.tile_size = tm.getTileSize();
const int M = Defaults::Editor::PICKER_MARGIN;
const int INNER_W = GameCanvas::WIDTH - (2 * M);
const int INNER_H = GameCanvas::HEIGHT - (2 * M);
if (lay.tile_size <= 0) { return lay; }
const int MAX_COLS = INNER_W / lay.tile_size;
lay.rows_visible = INNER_H / lay.tile_size;
int tcols = 0;
const auto TILESET = Resource::Cache::get().getSurface(tm.getTilesetPath());
if (TILESET != nullptr) {
tcols = TILESET->getWidth() / lay.tile_size;
const int TROWS = TILESET->getHeight() / lay.tile_size;
lay.total_tiles = tcols * TROWS;
}
// Com que els índexs del tileset són row-major (idx = row·TCOLS + col),
// fer servir cols = TCOLS reprodueix exactament la disposició del GIF.
const bool FITS_W = (tcols > 0 && tcols <= MAX_COLS);
lay.cols = FITS_W ? tcols : MAX_COLS;
lay.w = lay.cols * lay.tile_size;
lay.h = lay.rows_visible * lay.tile_size;
// Centrat horitzontalment quan la graella natural és més estreta;
// verticalment mantenim el marge superior i deixem scroll si cal.
lay.x = M + ((INNER_W - lay.w) / 2);
lay.y = M;
const int TOTAL_ROWS = (lay.cols > 0) ? ((lay.total_tiles + lay.cols - 1) / lay.cols) : 0;
lay.max_scroll = std::max(0, TOTAL_ROWS - lay.rows_visible);
return lay;
}
} // namespace
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;
picker_open_ = false;
// El writer salva sempre sobre la sala actualment carregada (segons
// el LevelManager): així el "9" de l'editor desa el que tens davant,
// no una constant fixa. Resolem la ruta via Resource::Helper perquè
// en Debug el root de dades és AEE_SOURCE_DIR (ruta absoluta al repo),
// no el CWD; sense això, el writer obriria el path relatiu al CWD i
// fallaria si el binari no es llança des del repo root.
const auto ABS_PATH = Resource::Helper::resolve(Levels::LevelManager::get().currentPath());
const bool SAVED = tilemap.save(ABS_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();
}
}
void Editor::update(int /*delta_ms*/, Tilemap& tilemap) {
if (!active_) { return; }
// Posició del ratolí en coordenades del canvas (independent del
// zoom de finestra i del mode d'escalat).
float mouse_x = 0.0F;
float mouse_y = 0.0F;
SDL_GetMouseState(&mouse_x, &mouse_y);
const auto CP = Screen::get().windowToCanvas(mouse_x, mouse_y);
const int CX = static_cast<int>(CP.x);
const int CY = static_cast<int>(CP.y);
// Consumim tecles pròpies del picker abans que cap altre handler
// (GlobalInputs) les vegi. ESC només es consumeix si el picker està
// obert — fora d'ell segueix arribant a GlobalInputs per la seva
// acció de confirmar-eixida.
const SDL_Scancode PICKER_SC = KeyConfig::get().scancode("editor_picker");
for (auto it = EventBuffer::events.begin(); it != EventBuffer::events.end();) {
bool consumed = false;
if (it->type == SDL_EVENT_KEY_DOWN) {
if (PICKER_SC != SDL_SCANCODE_UNKNOWN && it->key.scancode == PICKER_SC) {
picker_open_ = !picker_open_;
if (picker_open_) { picker_scroll_ = 0; }
consumed = true;
} else if (picker_open_ && it->key.scancode == SDL_SCANCODE_ESCAPE) {
picker_open_ = false;
consumed = true;
}
}
if (consumed) {
it = EventBuffer::events.erase(it);
} else {
++it;
}
}
if (picker_open_) {
updatePicker(tilemap, CX, CY);
} else {
updateMap(tilemap, CX, CY);
}
}
void Editor::updateMap(Tilemap& tilemap, int canvas_x, int canvas_y) {
// Cursor → cel·la del tilemap. Fora del mapa deixem -1/-1 perquè
// `render()` sàpiga no dibuixar preview i `setTileAt` ignori el clic.
const int TS = tilemap.getTileSize();
const int MX = canvas_x - tilemap.getOriginX();
const int MY = canvas_y - tilemap.getOriginY();
cursor_col_ = -1;
cursor_row_ = -1;
if (TS > 0 && MX >= 0 && MY >= 0) {
const int C = MX / TS;
const int R = MY / TS;
if (C < tilemap.getCols() && R < tilemap.getRows()) {
cursor_col_ = C;
cursor_row_ = R;
}
}
for (const auto& ev : EventBuffer::events) {
if (ev.type != SDL_EVENT_MOUSE_BUTTON_DOWN) { continue; }
if (ev.button.button == SDL_BUTTON_LEFT) {
if (brush_ >= 0 && cursor_col_ >= 0 && cursor_row_ >= 0) {
tilemap.setTileAt(cursor_col_, cursor_row_, brush_);
}
} else if (ev.button.button == SDL_BUTTON_RIGHT) {
brush_ = -1;
}
}
}
void Editor::updatePicker(const Tilemap& tilemap, int canvas_x, int canvas_y) {
const auto LAY = layoutPicker(tilemap);
picker_hover_ = -1;
if (canvas_x >= LAY.x && canvas_x < LAY.x + LAY.w && canvas_y >= LAY.y && canvas_y < LAY.y + LAY.h && LAY.tile_size > 0) {
const int VC = (canvas_x - LAY.x) / LAY.tile_size;
const int VR = (canvas_y - LAY.y) / LAY.tile_size;
const int IDX = ((picker_scroll_ + VR) * LAY.cols) + VC;
if (IDX < LAY.total_tiles) { picker_hover_ = IDX; }
}
for (const auto& ev : EventBuffer::events) {
if (ev.type == SDL_EVENT_MOUSE_WHEEL) {
picker_scroll_ -= static_cast<int>(ev.wheel.y);
picker_scroll_ = std::clamp(picker_scroll_, 0, LAY.max_scroll);
} else if (ev.type == SDL_EVENT_MOUSE_BUTTON_DOWN && ev.button.button == SDL_BUTTON_LEFT) {
if (picker_hover_ >= 0) {
brush_ = picker_hover_;
picker_open_ = false;
}
}
}
}
void Editor::render(const Tilemap& tilemap) const {
if (!active_) { return; }
auto& screen = Screen::get();
constexpr int W = GameCanvas::WIDTH;
constexpr int H = GameCanvas::HEIGHT;
if (picker_open_) {
renderPicker(tilemap);
} else if (brush_ >= 0 && cursor_col_ >= 0 && cursor_row_ >= 0) {
// Preview del brush sota el cursor (només quan el picker és tancat).
const int PX = tilemap.getOriginX() + (cursor_col_ * tilemap.getTileSize());
const int PY = tilemap.getOriginY() + (cursor_row_ * tilemap.getTileSize());
tilemap.renderTilePreview(PX, PY, brush_);
}
constexpr auto BORDER = Defaults::Editor::BORDER_ARGB;
screen.fillRect(0, 0, W, 1, BORDER);
screen.fillRect(0, H - 1, W, 1, BORDER);
screen.fillRect(0, 0, 1, H, BORDER);
screen.fillRect(W - 1, 0, 1, H, BORDER);
}
void Editor::renderPicker(const Tilemap& tilemap) const {
const auto LAY = layoutPicker(tilemap);
auto& screen = Screen::get();
screen.fillRect(LAY.x, LAY.y, LAY.w, LAY.h, Defaults::Editor::PICKER_BG_ARGB);
for (int vr = 0; vr < LAY.rows_visible; ++vr) {
const int LOGICAL_ROW = picker_scroll_ + vr;
for (int vc = 0; vc < LAY.cols; ++vc) {
const int IDX = (LOGICAL_ROW * LAY.cols) + vc;
if (IDX >= LAY.total_tiles) { break; }
const int PX = LAY.x + (vc * LAY.tile_size);
const int PY = LAY.y + (vr * LAY.tile_size);
tilemap.renderTilePreview(PX, PY, IDX);
}
}
if (picker_hover_ >= 0 && LAY.cols > 0) {
const int VR = (picker_hover_ / LAY.cols) - picker_scroll_;
const int VC = picker_hover_ % LAY.cols;
if (VR >= 0 && VR < LAY.rows_visible) {
const int PX = LAY.x + (VC * LAY.tile_size);
const int PY = LAY.y + (VR * LAY.tile_size);
const int TS = LAY.tile_size;
constexpr auto C = Defaults::Editor::PICKER_HOVER_ARGB;
screen.fillRect(PX, PY, TS, 1, C);
screen.fillRect(PX, PY + TS - 1, TS, 1, C);
screen.fillRect(PX, PY, 1, TS, C);
screen.fillRect(PX + TS - 1, PY, 1, TS, C);
}
}
}
#endif // _DEBUG