#include "editor.hpp" #include "serialize/json-helper.hpp" #include "serialize/tile-atlas.hpp" #include "src/loader.hpp" #include "random.hpp" #include "compat/assert.hpp" #include "compat/unreachable.hpp" #include "src/tile-defs.hpp" #include "src/world.hpp" #include "keys.hpp" #include #include #include namespace floormat { tile_editor::tile_editor(editor_mode mode, StringView name) : _name{ name}, _mode{ mode} { load_atlases(); } void tile_editor::load_atlases() { static const std::filesystem::path image_path{IMAGE_PATH, std::filesystem::path::generic_format}; using atlas_array = std::vector>; for (auto& atlas : json_helper::from_json(image_path/(_name + ".json"))) { StringView name = atlas->name(); if (auto x = name.findLast('.'); x) name = name.prefix(x.data()); auto& [_, vec] = _permutation; vec.reserve((std::size_t)atlas->num_tiles()); _atlases[name] = std::move(atlas); } } std::shared_ptr tile_editor::maybe_atlas(StringView str) { auto it = std::find_if(_atlases.begin(), _atlases.end(), [&](const auto& tuple) -> bool { const auto& [x, _] = tuple; return StringView{x} == str; }); if (it == _atlases.end()) return nullptr; else return it->second; } std::shared_ptr tile_editor::atlas(StringView str) { if (auto ptr = maybe_atlas(str); ptr) return ptr; else fm_abort("no such atlas: %s", str.cbegin()); } void tile_editor::clear_selection() { _selected_tile = {}; _permutation = {}; _selection_mode = sel_none; } void tile_editor::select_tile(const std::shared_ptr& atlas, std::size_t variant) { fm_assert(atlas); clear_selection(); _selection_mode = sel_tile; _selected_tile = { atlas, variant_t(variant % atlas->num_tiles()) }; } void tile_editor::select_tile_permutation(const std::shared_ptr& atlas) { fm_assert(atlas); clear_selection(); _selection_mode = sel_perm; _permutation = { atlas, {} }; } bool tile_editor::is_tile_selected(const std::shared_ptr& atlas, std::size_t variant) const { return atlas && _selection_mode == sel_tile && _selected_tile && atlas == _selected_tile.atlas && variant == _selected_tile.variant; } bool tile_editor::is_permutation_selected(const std::shared_ptr& atlas) const { const auto& [perm, _] = _permutation; return atlas && _selection_mode == sel_perm && perm == atlas; } bool tile_editor::is_atlas_selected(const std::shared_ptr& atlas) const { switch (_selection_mode) { default: case sel_none: return false; case sel_perm: return is_permutation_selected(atlas); case sel_tile: return atlas && _selected_tile && atlas == _selected_tile.atlas; } } template void fisher_yates(T begin, T end) { const auto N = std::distance(begin, end); for (auto i = N-1; i >= 1; i--) { const auto j = random(i+1); using std::swap; swap(begin[i], begin[j]); } } tile_image_proto tile_editor::get_selected_perm() { auto& [atlas, vec] = _permutation; const auto N = (variant_t)atlas->num_tiles(); if (N == 0) return {}; if (vec.empty()) { for (variant_t i = 0; i < N; i++) vec.push_back(i); fisher_yates(vec.begin(), vec.end()); } const auto idx = vec.back(); vec.pop_back(); return {atlas, idx}; } tile_image_proto tile_editor::get_selected() { switch (_selection_mode) { case sel_none: return {}; case sel_tile: return _selected_tile; case sel_perm: return get_selected_perm(); default: fm_warn_once("invalid editor mode '%u'", (unsigned)_selection_mode); break; } } void tile_editor::place_tile(world& world, global_coords pos, const tile_image_proto& img) { auto [c, t] = world[pos]; switch (_mode) { case editor_mode::none: break; case editor_mode::floor: t.ground() = img; break; case editor_mode::walls: switch (_rotation) { case editor_wall_rotation::N: t.wall_north() = img; break; case editor_wall_rotation::W: t.wall_west() = img; break; } break; } } void tile_editor::toggle_rotation() { if (_rotation == editor::rotation_W) _rotation = editor::rotation_N; else _rotation = editor::rotation_W; } void tile_editor::set_rotation(editor_wall_rotation r) { switch (r) { default: fm_warn_once("invalid rotation '0x%hhx", r); return; case editor::rotation_W: case editor::rotation_N: _rotation = r; break; } } auto tile_editor::check_snap(int mods) const -> snap_mode { const bool ctrl = mods & kmod_ctrl, shift = mods & kmod_shift; if (!(ctrl | shift)) return snap_mode::none; switch (_mode) { default: case editor_mode::none: return snap_mode::none; case editor_mode::walls: switch (_rotation) { case editor_wall_rotation::N: return snap_mode::horizontal; case editor_wall_rotation::W: return snap_mode::vertical; default: return snap_mode::none; } case editor_mode::floor: if (shift) return snap_mode::horizontal; if (ctrl) return snap_mode::vertical; return snap_mode::none; } } bool tile_editor::can_rotate() const { return _mode == editor_mode::walls; } editor::editor() { } void editor::set_mode(editor_mode mode) { _mode = mode; on_release(); } const tile_editor* editor::current() const noexcept { switch (_mode) { case editor_mode::floor: return &_floor; case editor_mode::walls: return &_wall; // todo default: return nullptr; } } tile_editor* editor::current() noexcept { return const_cast(static_cast(*this).current()); } void editor::on_release() { _last_pos = std::nullopt; } auto editor::get_snap_value(snap_mode snap, int mods) const -> snap_mode { if (const auto* mode = current(); mode != nullptr) return mode->check_snap(mods); else if (snap != snap_mode::none) return snap; else return snap_mode::none; } global_coords editor::apply_snap(global_coords pos, global_coords last, snap_mode snap) noexcept { switch (snap) { default: break; case snap_mode::horizontal: pos.y = last.y; break; case snap_mode::vertical: pos.x = last.x; break; } return pos; } void editor::on_mouse_move(world& world, global_coords& pos, int mods) { if (auto* mode = current(); mode && _last_pos && _last_pos->btn != button::none) { auto& last = *_last_pos; const Vector2i offset = pos - last.coord; const snap_mode snap = get_snap_value(last.snap, mods); const global_coords draw_coord = apply_snap(last.draw_coord + offset, last.draw_coord, snap); if (draw_coord != last.draw_coord) { const auto draw_offset = draw_coord - last.draw_coord; if (!!draw_offset[0] ^ !!draw_offset[1] && std::abs(draw_offset.sum()) > 1) { const auto [minx, maxx] = std::minmax(draw_coord.x, last.draw_coord.x); const auto [miny, maxy] = std::minmax(draw_coord.y, last.draw_coord.y); if (draw_offset[0]) for (std::uint32_t i = minx; i <= maxx; i++) on_click_(world, { i, draw_coord.y }, last.btn); else for (std::uint32_t j = miny; j <= maxy; j++) on_click_(world, { draw_coord.x, j }, last.btn); } else on_click_(world, draw_coord, last.btn); _last_pos = { pos, draw_coord, snap, last.btn }; } pos = draw_coord; } } void editor::on_click_(world& world, global_coords pos, button b) { if (auto* mode = current(); mode != nullptr) if (auto opt = mode->get_selected(); opt) { switch (b) { case button::place: return mode->place_tile(world, pos, opt); case button::remove: return mode->place_tile(world, pos, {}); default: break; } } on_release(); } void editor::on_click(world& world, global_coords pos, int mods, button b) { if (auto* mode = current(); mode != nullptr) { _last_pos = { pos, pos, mode->check_snap(mods), b }; on_click_(world, pos, b); } } } // namespace floormat